Repository navigation
Fused QKV ROPE - CS FP8 quantization #3675
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
vasunvidia
wants to merge
3
commits into
NVIDIA:main
Choose a base branch
from
vasunvidia:vrengasamy/fused_rope_optimization
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # | ||
| # See LICENSE for license information. | ||
|
|
||
| """Two-phase FP8 current-scaling quantization kernels written with OpenAI Triton.""" | ||
|
|
||
| import triton | ||
| import triton.language as tl | ||
|
|
||
| try: | ||
| from triton.language.extra import libdevice | ||
| except ImportError: # older Triton | ||
| from triton.language.extra.cuda import libdevice | ||
|
|
||
|
|
||
| @triton.jit | ||
| def _finalize_scale(ws_ptr, scale_inv_ptr, n_prog, eps, FP8MAX: tl.constexpr, POW2: tl.constexpr): | ||
| """Executed by every CTA after its amax atomic; the last one turns amax into the scale. | ||
|
|
||
| Mirrors TE's compute_scale_from_amax. The workspace ``ws = [amax, counter, scale]`` is | ||
| handed back zeroed so no memset launch is needed (CUDA-graph safe). | ||
| """ | ||
| cnt = tl.atomic_add(ws_ptr + 1, 1.0) | ||
| if cnt == n_prog - 1: | ||
| amax = tl.atomic_xchg(ws_ptr, 0.0) | ||
| tl.atomic_xchg(ws_ptr + 1, 0.0) | ||
| bad = (amax == float("inf")) | (amax != amax) | ||
| amax = tl.where(amax < eps, eps, amax) | ||
| bad = bad | (amax == 0.0) | ||
| scale = libdevice.div_rn(FP8MAX, amax) | ||
| scale = tl.where(scale == float("inf"), 3.4028234663852886e38, scale) | ||
| if POW2: | ||
| scale = (scale.to(tl.uint32, bitcast=True) & 0xFF800000).to(tl.float32, bitcast=True) | ||
| scale = tl.where(bad, 1.0, scale) | ||
| tl.store(ws_ptr + 2, scale) | ||
| tl.store(scale_inv_ptr, libdevice.rcp_rn(scale)) | ||
|
|
||
|
|
||
| @triton.jit | ||
| def current_scaling_quantize_kernel( | ||
| x_ptr, | ||
| out_ptr, | ||
| ws_ptr, | ||
| scale_inv_ptr, | ||
| n, | ||
| eps, | ||
| BLOCK: tl.constexpr, | ||
| CAST: tl.constexpr, | ||
| FP8MAX: tl.constexpr, | ||
| POW2: tl.constexpr, | ||
| ): | ||
| """CAST=False: persistent amax pass, one atomic per CTA, last CTA computes the scale. | ||
| CAST=True: one block per CTA, in reverse order so the amax pass's tail is still in L2.""" | ||
| n_prog = tl.num_programs(0) | ||
| pid = tl.program_id(0) | ||
| if CAST: | ||
| offs = (n_prog - 1 - pid).to(tl.int64) * BLOCK + tl.arange(0, BLOCK) | ||
| m = offs < n | ||
| x = tl.load(x_ptr + offs, mask=m, other=0.0).to(tl.float32) | ||
| scale = tl.load(ws_ptr + 2) | ||
| tl.store(out_ptr + offs, (x * scale).to(out_ptr.dtype.element_ty), mask=m) | ||
| else: | ||
| amax = tl.zeros([BLOCK], tl.float32) | ||
| for blk in tl.range(pid, tl.cdiv(n, BLOCK), n_prog): | ||
| offs = blk.to(tl.int64) * BLOCK + tl.arange(0, BLOCK) | ||
| x = tl.load(x_ptr + offs, mask=offs < n, other=0.0).to(tl.float32) | ||
| amax = tl.maximum(amax, tl.abs(x)) | ||
| tl.atomic_max(ws_ptr, tl.max(amax, axis=0)) | ||
| _finalize_scale(ws_ptr, scale_inv_ptr, n_prog, eps, FP8MAX, POW2) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| # Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # | ||
| # See LICENSE for license information. | ||
|
|
||
| """Fused RoPE + FP8 current-scaling quantization of Q/K/V, written with OpenAI Triton. | ||
|
|
||
| Input is the QKV GEMM output ``[s, b, g, (hq + 2) * d]``. Two passes are needed because current | ||
| scaling requires the global amax before the first cast: | ||
|
|
||
| * amax pass: persistent; rotates Q/K (rounded to bf16 like TE's RoPE output) and reduces | ||
| amax(|Q|, |K|, |V|) with one atomic per CTA; the last CTA turns the amax into the scale. | ||
| * cast pass: rotates again and writes e4m3/e5m2 into the packed ``[Q | K | V]`` payload, visiting | ||
| tiles in reverse order so the tail of the amax pass is still in L2. | ||
|
|
||
| Results are bitwise identical to ``tex.fused_qkv_rope_forward`` followed by current-scaling | ||
| quantization. | ||
| """ | ||
|
|
||
| import triton | ||
| import triton.language as tl | ||
|
|
||
| from .current_scaling import _finalize_scale | ||
|
|
||
| try: | ||
| from triton.language.extra import libdevice | ||
| except ImportError: # older Triton | ||
| from triton.language.extra.cuda import libdevice | ||
|
|
||
|
|
||
| @triton.jit | ||
| def rope_trig_table_kernel(f_ptr, cos_ptr, sin_ptr, n, BLOCK: tl.constexpr): | ||
| """cos/sin of the RoPE angles with the libdevice functions TE's sincosf resolves to, | ||
| evaluated once per angle instead of once per element of every head.""" | ||
| offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) | ||
| m = offs < n | ||
| f = tl.load(f_ptr + offs, mask=m, other=0.0) | ||
| tl.store(cos_ptr + offs, libdevice.cos(f), mask=m) | ||
| tl.store(sin_ptr + offs, libdevice.sin(f), mask=m) | ||
|
|
||
|
|
||
| @triton.jit | ||
| def _rot(x, c, xr, s): | ||
| # fma(x, c, xr * s): matches nvcc's contraction in TE's RoPE kernels | ||
| return tl.fma(x, c, xr * s) | ||
|
|
||
|
|
||
| @triton.jit | ||
| def _rot_fwd(lo, hi, c_lo, s_lo, c_hi, s_hi): | ||
| # TE fused_qkv_rope_block_forward: out[d<h] = x[d]cos[d] + (-x[d+h])sin[d]; | ||
| # out[d>=h] = x[d]cos[d] + x[d-h]sin[d]; bf16 output | ||
| y_lo = _rot(lo, c_lo, -hi, s_lo).to(tl.bfloat16).to(tl.float32) | ||
| y_hi = _rot(hi, c_hi, lo, s_hi).to(tl.bfloat16).to(tl.float32) | ||
| return y_lo, y_hi | ||
|
|
||
|
|
||
| @triton.jit | ||
| def _rope_qkv_token( | ||
| x_ptr, | ||
| cos_ptr, | ||
| sin_ptr, | ||
| out_ptr, | ||
| sc, | ||
| tok, | ||
| n_tok, | ||
| B: tl.constexpr, | ||
| G: tl.constexpr, | ||
| HQ: tl.constexpr, | ||
| D: tl.constexpr, | ||
| CAST: tl.constexpr, | ||
| ): | ||
| """Rotate Q/K of one (s, b) token for all heads. CAST=False returns the token's amax; | ||
| CAST=True writes the scaled FP8 values into the packed [Q | K | V] payload.""" | ||
| W: tl.constexpr = (HQ + 2) * D | ||
| HD: tl.constexpr = D // 2 | ||
| pos = tok // B | ||
| g = tl.arange(0, G) | ||
| jq = tl.arange(0, HQ) | ||
| dl = tl.arange(0, HD) | ||
| d = tl.arange(0, D) | ||
| tb = pos * D + dl | ||
| c_lo = tl.load(cos_ptr + tb) | ||
| s_lo = tl.load(sin_ptr + tb) | ||
| c_hi = tl.load(cos_ptr + tb + HD) | ||
| s_hi = tl.load(sin_ptr + tb + HD) | ||
| # Q: [g, jq, dl]; K: [g, dl]; V: [g, d] | ||
| row = tok * (G * W) + g * W | ||
| q_src = x_ptr + row[:, None, None] + jq[None, :, None] * D + dl[None, None, :] | ||
| k_src = x_ptr + row[:, None] + HQ * D + dl[None, :] | ||
| v_src = x_ptr + row[:, None] + (HQ + 1) * D + d[None, :] | ||
| q_lo, q_hi = _rot_fwd( | ||
| tl.load(q_src).to(tl.float32), | ||
| tl.load(q_src + HD).to(tl.float32), | ||
| c_lo[None, None, :], | ||
| s_lo[None, None, :], | ||
| c_hi[None, None, :], | ||
| s_hi[None, None, :], | ||
| ) | ||
| k_lo, k_hi = _rot_fwd( | ||
| tl.load(k_src).to(tl.float32), | ||
| tl.load(k_src + HD).to(tl.float32), | ||
| c_lo[None, :], | ||
| s_lo[None, :], | ||
| c_hi[None, :], | ||
| s_hi[None, :], | ||
| ) | ||
| v = tl.load(v_src).to(tl.float32) | ||
| if CAST: | ||
| nq = n_tok * (G * HQ * D) | ||
| nk = n_tok * (G * D) | ||
| q_dst = ( | ||
| out_ptr | ||
| + tok * (G * HQ * D) | ||
| + g[:, None, None] * (HQ * D) | ||
| + jq[None, :, None] * D | ||
| + dl[None, None, :] | ||
| ) | ||
| k_dst = out_ptr + nq + tok * (G * D) + g[:, None] * D + dl[None, :] | ||
| v_dst = out_ptr + nq + nk + tok * (G * D) + g[:, None] * D + d[None, :] | ||
| ety = out_ptr.dtype.element_ty | ||
| tl.store(q_dst, (q_lo * sc).to(ety)) | ||
| tl.store(q_dst + HD, (q_hi * sc).to(ety)) | ||
| tl.store(k_dst, (k_lo * sc).to(ety)) | ||
| tl.store(k_dst + HD, (k_hi * sc).to(ety)) | ||
| tl.store(v_dst, (v * sc).to(ety)) | ||
| return 0.0 | ||
| else: | ||
| aq = tl.max(tl.max(tl.max(tl.maximum(tl.abs(q_lo), tl.abs(q_hi)), 2), 1), 0) | ||
| ak = tl.max(tl.max(tl.maximum(tl.abs(k_lo), tl.abs(k_hi)), 1), 0) | ||
| av = tl.max(tl.max(tl.abs(v), 1), 0) | ||
| return tl.maximum(tl.maximum(aq, ak), av) | ||
|
|
||
|
|
||
| @triton.jit | ||
| def rope_qkv_quantize_kernel( | ||
| x_ptr, | ||
| cos_ptr, | ||
| sin_ptr, | ||
| out_ptr, | ||
| ws_ptr, | ||
| scale_inv_ptr, | ||
| n_tok, | ||
| eps, | ||
| B: tl.constexpr, | ||
| G: tl.constexpr, | ||
| HQ: tl.constexpr, | ||
| D: tl.constexpr, | ||
| CAST: tl.constexpr, | ||
| FP8MAX: tl.constexpr, | ||
| POW2: tl.constexpr, | ||
| NUM_STAGES: tl.constexpr = 1, | ||
| ): | ||
| """CAST=False: persistent amax pass (one atomic per CTA, last CTA computes the scale). | ||
| CAST=True: one token per CTA, in the reverse order of the amax pass.""" | ||
| n_prog = tl.num_programs(0) | ||
| pid = tl.program_id(0) | ||
| if CAST: | ||
| sc = tl.load(ws_ptr + 2) | ||
| _rope_qkv_token( | ||
| x_ptr, cos_ptr, sin_ptr, out_ptr, sc, n_prog - 1 - pid, n_tok, B, G, HQ, D, True | ||
| ) | ||
| else: | ||
| amax = 0.0 | ||
| for tok in tl.range(pid, n_tok, n_prog, num_stages=NUM_STAGES): | ||
| a = _rope_qkv_token( | ||
| x_ptr, cos_ptr, sin_ptr, out_ptr, 1.0, tok, n_tok, B, G, HQ, D, False | ||
| ) | ||
| amax = tl.maximum(amax, a) | ||
| tl.atomic_max(ws_ptr, amax) | ||
| _finalize_scale(ws_ptr, scale_inv_ptr, n_prog, eps, FP8MAX, POW2) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GandHQare passed directly totl.arange(), which requires power-of-two range sizes. The deferred input checks allow other head counts, such as 12 query heads across 3 groups (G=3,HQ=4). These inputs reach the kernel and fail to compile instead of using the promised fallback. Pad these ranges and mask their loads and stores, or reject unsupported counts before returning placeholders.