Skip to content

Apply the RPBH pre-round inside the 8-bit activation kernel (0.16.1) - #26

Merged
iamwavecut merged 1 commit into
mainfrom
perf/fused-pre-rotation
Nov 3, 2026
Merged

iamwavecut merged 1 commit into
mainfrom
perf/fused-pre-rotation

Conversation

@iamwavecut

Copy link
Copy Markdown
Owner

The 8-bit absmax activation kernel now applies the first RPBH round of rotation_rounds="auto"/2 in its own row pass. 0.16.0 ran that round as a separate RPBHRotator kernel, which wrote the rotated input back to memory. On Fibo 2.0 Dev W4A8 that pass cost 11% of the time per image.

Changes

  • _rpbh_absmax_int8_kernel loads each row coalesced and permutes it in shared memory (tl.gather, Triton >= 3.2). With the pre-round it applies the first RPBH round in FP32 and rounds to bf16 once, as RPBHRotator did, then applies the paper round, the absmax and the INT8 store. Inputs up to 16384 wide use it; wider inputs keep RPBHRotator in front.
  • A plain bf16 input skips the token-norm prologue pass whose norms the scales overwrote. The kernel reads the row at any row stride, so the strided Fibo 2.0 FFN-out view needs no copy.
  • RPBHRotator runs blocks below 512 as one row per program. This affects the Lloyd-Max two-round path. At d = 9536 and 4096 rows the rotation time drops from 0.37 ms to 0.10 ms.
  • num_warps of the absmax row kernel depends on the row width and the RPBH block. The values come from a sweep on the RTX 5090.
  • OrbitQuantLinear passes the unrotated input to the CUDA absmax quantizer, and the quantizer applies both rounds.
  • The version is 0.16.1.

Bit identity

  • The CUDA tests compare the fused path against the 0.16.0 order: prologue, bf16, RPBHRotator, then the one-round kernel. They cover d = 1920, 3840, 9536 and 16640 with 9 input cases each: plain, strided, fp32, norm_modulate, rms_scale_indexed, rms_plain_shift_indexed, layer_one_plus, rms_torch and multiply.
  • The one-round absmax test now requires exact equality with an IEEE reference (it allowed a difference of 1 before).
  • The benchmark loaded the 0.16.0 module next to this branch. Over 60 configurations, codes and scales were equal everywhere. The num_warps sweep produced identical codes for every warp count.

Timing

RTX 5090, torch 2.12.1+cu130, Triton 3.7.1. Values are ms per call, the median of 5 batches of 50 calls.

d rows 0.16.0, 1 round 0.16.0, 2 rounds this PR, 1 round this PR, 2 rounds
9536 4096 0.217 0.567 0.165 0.224
9536 4608 0.247 0.643 0.185 0.252
9536 9216 0.482 1.308 0.360 0.491
9536, strided (Fibo FFN-out) 4608 0.288 0.756 0.185 0.252
3840 4096 0.076 0.124 0.049 0.049
3840 9216 0.101 0.284 0.065 0.096
13568 4096 0.275 0.652 0.175 0.254
13568 9216 0.598 1.470 0.371 0.556
1920 9216 0.077 0.125 0.048 0.049
3360 9216 0.079 0.546 0.064 0.091
9536, RMS norm + scale prologue 4096 0.253 0.652 0.247 0.306
9536, RMS norm + scale prologue 9216 0.584 1.515 0.572 0.703
13568, RMS norm + scale prologue 9216 0.750 1.771 0.681 0.861

At 4096-9216 rows and d = 1920, 3840, 9536 and 13568, the two-round quantizer takes at most 4% longer than the 0.16.0 one-round quantizer. In CUDA graph replay, which excludes the CPU launch cost, the difference is at most +8% (d = 1920).

Not reached

  • With a norm or modulation prologue, the two-round quantizer takes 15-22% longer than the 0.16.0 one-round quantizer at d = 9536 and d = 13568. The prologue keeps its own kernel. A single kernel would have to compute the prologue's RMS reduction in the same order as the 8-warp prologue kernel to stay bit-identical.
  • At d = 3360 (h = 32) and 9216 rows, the two-round quantizer takes 16-19% longer than the 0.16.0 one-round quantizer.
  • In launch-bound calls (256 rows), the one-round path with a prologue costs about 5 µs more CPU time per call, most likely from the kernel's longer argument list. The GPU time is the same or lower.

Gates

  • uv run ruff check . passes. uv run pytest -ra: 694 passed, 210 skipped.
  • scripts/run_paper_methodology_checks.sh, scripts/run_hf_compat_checks.sh --mode all and the MPS gate on Apple Silicon pass.
  • On an RTX 5090 (Vast.ai, torch 2.12.1+cu130), scripts/run_cuda_kernel_checks.sh passes, including the prebuilt native package.
  • The CUDA pytest set passes except tests/test_shared_projection.py::test_real_cuda_projection_reuse_is_bit_identical. That test fails the same way on 0.16.0 on that host: no native packed-matmul package was provisioned, and auto_fused falls back to the per-forward dequant.

The absmax INT8 activation kernel now applies the first RPBH round of
rotation_rounds="auto"/2 in its own row pass instead of a separate
RPBHRotator kernel. It loads each row coalesced, permutes it in shared
memory (tl.gather), applies the round in FP32 and rounds to bf16 once,
as the separate pass did. Codes and scales match 0.16.0 bit for bit for
plain, strided and fp32 inputs and every prologue variant.

RTX 5090, torch 2.12.1+cu130, 4096-9216 tokens, per call:
- d = 9536: two rounds 0.22-0.49 ms (0.16.0: 0.57-1.31 ms; 0.16.0 with
  one round: 0.22-0.48 ms).
- d = 3840: two rounds 0.05-0.10 ms (0.16.0: 0.12-0.28 ms; one round
  0.08-0.10 ms).
- A norm or modulation prologue keeps its own kernel. With one, the two
  rounds take 15-22% longer than the 0.16.0 one-round quantizer at
  d = 9536 and d = 13568.

Other changes:
- The one-round absmax quantizer reads a plain bf16 row coalesced and
  skips the token-norm pass whose norms the scales overwrote: 19-38% less
  time at 4096-9216 tokens.
- RPBHRotator runs blocks below 512 as one row per program (d = 9536,
  4096 rows: 0.37 ms -> 0.10 ms), which speeds up the Lloyd-Max
  two-round path.
- num_warps of the absmax row kernel follows the row width and the RPBH
  block (measured: 256-wide blocks run 6-31% faster on 4 warps).
- OrbitQuantLinear hands the unrotated input to the CUDA absmax
  quantizer.
@iamwavecut
iamwavecut merged commit 62247ac into main Nov 3, 2026
3 checks passed
@iamwavecut
iamwavecut deleted the perf/fused-pre-rotation branch November 3, 2026 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant