Skip to content

prefill: the fused int8 prompt experts on Turing (sm_75), opt-in (+9 % prefill on RTX 2080 Ti) - #2145

Closed
rafatxf wants to merge 1 commit into
Niko1221:mainfrom
rafatxf:pr/sm75-fused-experts
Closed

rafatxf wants to merge 1 commit into
Niko1221:mainfrom
rafatxf:pr/sm75-fused-experts

Conversation

@rafatxf

@rafatxf rafatxf commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

On Turing the prompt path's experts always go through llama.cpp's MMQ: the fused int8 kernels (moe_fused.cu for the Strata Q2_0 pack, moe_fused_iq.cu's native_kernel for the native packs, #136) need sm_80. On an RTX 2080 Ti MMQ's Q2_0 kernel runs at ~20-30 TOPS (255 registers with spills, one CTA per SM) and is the largest single cost of a long prompt read.

This ports both to sm_75, opt-in on top of STRATA_PF_FUSED=1 with STRATA_PF_FUSED_SM75=1:

  • cp.async → plain 16-byte copies (the barriers around them stay), prefetch.global.L2 kept;
  • mma m16n8k32 / m16n8k16 with s8 operands → four / two m8n8k16 (rows g / g + 8 by k 0-15 / 16-31). The int32 sums are exact in any order, so d is what the sm_80 instruction gives; MAGIC is passed as the C operand of each pair's first m8n8k16;
  • two activation stages below sm_80 (64 KB of shared memory per SM), smem_bytes / the launch / the occupancy query take the device's depth (host_astages());
  • native_kernel takes Q2_0 gate/up (64-value blocks: a block per stage, as the down rows already read theirs), so the canonical Q2_0 pack (Q2_0 / Q2_0 in every layer) runs fused. gu_covered() lists Q2_0 on CUDA;
  • on Turing only that pair is set up (DevInfo::t75: native_supported answers false for the others, which keep MMQ);
  • Turing, Q2_0 only (T75_DEV): a stage's two 32-value halves share the block's d, so a row's dot is d (dx_0 dot_0 + dx_1 dot_1) - 11 % fewer instructions (the kernel is bound by its FP32 epilogue on Turing, not the tensor pipe). sm_80+ keeps its code and bits.

prefill_fused_iq_test gets the Q2_0 / Q2_0 pair, and skips the pairs a Turing card does not set up instead of failing.

Setup

RTX 2080 Ti 22 GB (sm_75), i7-6700, CUDA 13.0, SM 1650 MHz, 240 W; v0.1.43. Canonical Q2_0 pack, --prefill auto:16384, STRATA_BF16_TC=1, STRATA_SELECT_SIMT=2.

Kernels (prefill_fused_iq_test --only="Q2_0 / Q2_0", one layer, 512 experts, top 10)

tokens MMQ path fused
2,048 12.99 ms 6.61 ms 1.96×
8,192 30.75 ms 21.18 ms* 1.45×
16,384 55.02 ms 37.15 ms 1.48×

* before the Q2_0 epilogue change. Fused vs MMQ rel RMS 1.6e-5 - 2.7e-5; both 1.09e-2 against the FP32 reference (the int8 activations' own error). prefill_fused_moe_test (Strata pack) passes too: 5.29 ms fused vs MMQ 3.67×, rel RMS 6.7e-5.

Engine

98K prompt (16K chunks, two runs each, deterministic run to run): 58.3 → 53.3 s (+9.4 %). 4K: 3.63 → 3.54 s.

Accuracy in the engine: layer 0 of a 32K prompt (identical input on both paths), every one of the 163,840 (token, expert) rows of the expert output against MMQ's: median rel 1.6e-7, p99.9 2.6e-3, max 7.6e-3 (the rows where H's int8 rounding falls the other way); against the FP16 expert path (STRATA_PREFILL_MMQ=0) MMQ and fused have the same error (median 1.08e-2).

Teacher-forced, as in #655 / #2071 (the first P tokens through the prompt path, the next 2,048 through the verify windows, STRATA_LOGPOS_TOPK=20):

P KL (MMQ ‖ fused) KL (FP16 ‖ MMQ) perplexity FP16 / MMQ / fused
16,384 0.0101 0.0090 7.308 / 7.255 / 7.501
32,768 0.0226 0.0110 4.561 / 4.557 / 4.354
65,536 0.0186 0.0262 4.110 / 4.096 / 4.122

The perplexity moves either way (+3.4 %, −4.5 %, +0.6 %) - trajectory noise: the QSA selection is discrete, so any change of the prompt path's rounding gives KLs of this size (enabling only one layer's fused experts already moves the first logit by KL ~1e-3, as much as the FP16 path in every layer). The verify phase's time over the same 2,048 tokens: MMQ 42.36 s, fused 42.28 s.

The server here will run with STRATA_PF_FUSED=1 STRATA_PF_FUSED_SM75=1.

🤖 Generated with Claude Code

The fused prompt-expert kernels (moe_fused.cu for the Strata Q2_0 pack,
moe_fused_iq.cu native_kernel for the native packs) on sm_75, with
STRATA_PF_FUSED=1 STRATA_PF_FUSED_SM75=1:

- cp.async -> plain 16-byte copies (the barriers around them stay),
  prefetch.global.L2 kept;
- mma m16n8k32 / m16n8k16 s8 -> four / two m8n8k16 (exact int32 sums,
  the same d as the sm_80 instructions), MAGIC as the C operand;
- two activation stages on Turing (64 KB of shared memory);
- native_kernel takes Q2_0 gate/up (64-value blocks, a block per stage),
  so the canonical Q2_0 pack (Q2_0 / Q2_0 layers) runs fused; on Turing
  only that pair is set up;
- Turing, Q2_0: the two halves of a stage share the weight scale, so a
  row's dot is d (dx_0 dot_0 + dx_1 dot_1) - 11 % fewer instructions.

RTX 2080 Ti, one layer, 512 experts top 10 (prefill_fused_iq_test,
Q2_0 / Q2_0 pair added): 2048 tokens MMQ 12.98 ms -> fused 6.59 ms,
16384 tokens 55.08 -> 37.16 ms; fused vs MMQ rel RMS 2e-5, the same
error against the FP32 reference.  In the engine (canonical Q2_0 pack,
98K prompt, 16K chunks): prefill 58.3 -> 53.3 s; layer 0 expert rows vs
MMQ: median rel 1.6e-7.  Teacher-forced 2048 tokens at P = 16K / 32K /
64K: perplexity vs MMQ +3.4 % / -4.5 % / +0.6 % (FP16 path vs MMQ
+0.7 % / +0.1 % / +0.3 %) - trajectory noise, no systematic shift; the
verify phase (same tokens) 42.36 s MMQ, 42.28 s fused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Niko1221 pushed a commit that referenced this pull request Oct 17, 2026
The fused prompt-expert kernels (moe_fused.cu for the Strata Q2_0 pack,
moe_fused_iq.cu native_kernel for the native packs) on sm_75, with
STRATA_PF_FUSED=1 STRATA_PF_FUSED_SM75=1:

- cp.async -> plain 16-byte copies (the barriers around them stay),
  prefetch.global.L2 kept;
- mma m16n8k32 / m16n8k16 s8 -> four / two m8n8k16 (exact int32 sums,
  the same d as the sm_80 instructions), MAGIC as the C operand;
- two activation stages on Turing (64 KB of shared memory);
- native_kernel takes Q2_0 gate/up (64-value blocks, a block per stage),
  so the canonical Q2_0 pack (Q2_0 / Q2_0 layers) runs fused; on Turing
  only that pair is set up;
- Turing, Q2_0: the two halves of a stage share the weight scale, so a
  row's dot is d (dx_0 dot_0 + dx_1 dot_1) - 11 % fewer instructions.

RTX 2080 Ti, one layer, 512 experts top 10 (prefill_fused_iq_test,
Q2_0 / Q2_0 pair added): 2048 tokens MMQ 12.98 ms -> fused 6.59 ms,
16384 tokens 55.08 -> 37.16 ms; fused vs MMQ rel RMS 2e-5, the same
error against the FP32 reference.  In the engine (canonical Q2_0 pack,
98K prompt, 16K chunks): prefill 58.3 -> 53.3 s; layer 0 expert rows vs
MMQ: median rel 1.6e-7.  Teacher-forced 2048 tokens at P = 16K / 32K /
64K: perplexity vs MMQ +3.4 % / -4.5 % / +0.6 % (FP16 path vs MMQ
+0.7 % / +0.1 % / +0.3 %) - trajectory noise, no systematic shift; the
verify phase (same tokens) 42.36 s MMQ, 42.28 s fused.
Niko1221 added a commit that referenced this pull request Oct 17, 2026
…capability through STRATA_EMULATE_CC like the other kernels
Niko1221 added a commit that referenced this pull request Oct 17, 2026
…) calls it; the HIP build of #2145 as submitted did not compile)
Niko1221 added a commit that referenced this pull request Oct 17, 2026
…n's only (sm_80+ with STRATA_PF_FUSED=1 is bit-identical to base again)

The z440 A4000 Q2_0 gate with STRATA_PF_FUSED=1 on both arms found the residual dump different from base: gu_covered() took Q2_0 on every CUDA card and d.ok on sm_80+ now required the Q2_0 gate/up setup.
@Niko1221

Copy link
Copy Markdown
Owner

Thanks rafatxf. Ported with your authorship plus three changes of ours. The shared launch used host_astages() which did not exist in the HIP build, so we added it. The capability check follows our emulation switch for tests. And your Q2_0 gate/up addition reached sm_80+: with STRATA_PF_FUSED=1 an RTX A4000 with the Q2_0 pack stopped matching the old output (and prompt speed dropped 4.5 % at 4K), so it is now limited to the Turing opt-in (STRATA_PF_FUSED=1 STRATA_PF_FUSED_SM75=1). With that, the default and the sm_80+ opt-in paths are byte-identical to before. We could not measure the +9 % without a Turing card.

@rafatxf

rafatxf commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Ported in 0.1.45, thanks! Turing numbers for STRATA_SM75_PERF (2x RTX 2080 Ti, Q2_0 and IQ3_S) are in #2069. Closing.

@rafatxf rafatxf closed this Oct 19, 2026
chaog992 added a commit to chaog992/Strata that referenced this pull request Oct 27, 2026
…/ Q2_0 layers on the fused int8 kernels on sm_80+

A host whose CPU lacks the AVX-512 set setup looks for gets the native Q2_0 pack, and on a CUDA card of sm_80 or newer
its Q2_0 gate/up layers read prompts through MMQ's Q2_0 kernel (its 128-wide tile: 255 registers with spills on
sm_120, ptxas -v), because b6443f3 kept Niko1221#2145's fused coverage of that format to the Turing opt-in: with
STRATA_PF_FUSED=1 an RTX A4000 stopped matching the old output and read 4K prompts 4.5% slower (the port note on
this opt-in sets it up and takes it for the Q2_0 / Q2_0 layers under its own variable, so STRATA_PF_FUSED=1 keeps its
bits and an unset engine is unchanged.  A ctest entry runs the kernel test's Q2_0 / Q2_0 pair with the variable alone
(the test's new --unset-pf-fused leaves STRATA_PF_FUSED unset instead of setting it to 1; 77 without a card and below
sm_80, where nothing is set up with it unset).

RTX 5080 (sm_120, 16 GB), i9-14900K (no AVX-512), Q2_0 native pack, 2,100 experts in VRAM, CPU prompt share off,
against the same build unset: at the same 4,864-token chunk (4 interleaved rounds) prompts of 4K, 16K and 23K tokens
read 2-3.5% faster; with --prefill auto (5 rounds) the fused path's smaller buffers make the chunk 5,632 instead of
4,864 and the 16K prompt reads 32-33% faster (2,592 -> 3,438 tok/s), the 23K one 14%, a 4K one 0-2%.  Per layer the
fused path is 1.3-1.9x the MMQ path with the same error against FP32 (prefill_fused_iq_test).  First-token KL against
the FP16 prompt path over 30 prompts of 1.5-4.6K tokens: fused 0.0110, MMQ 0.0112.  Unset, the first-token logits are
byte-identical to v0.2.2's.

Tests: prefill_fused_iq_test --only='Q2_0 / Q2_0' --unset-pf-fused with STRATA_PF_FUSED_Q2N=1 passes on the RTX 5080.
Niko1221 pushed a commit that referenced this pull request Oct 27, 2026
… sm_80+, STRATA_PF_FUSED_Q2N=1 (#3809)

A PC whose CPU lacks the AVX-512 set that setup checks for gets the native Q2_0 pack. On a CUDA card of sm_80 or newer its
Q2_0 / Q2_0 prompt experts run through MMQ, although the fused int8 kernel for that format (#2145) is in the binary and set
up only on a Turing card (b6443f3).

STRATA_PF_FUSED_Q2N=1 sets that kernel up on any sm_80+ card and takes it for the Q2_0 / Q2_0 layers, under its own variable
so that STRATA_PF_FUSED=1 keeps its bits. STRATA_PF_FUSED=0 and STRATA_PF_FUSED_NATIVE=0 still turn it off. A failed setup
leaves the occupancy as it was and the layers on MMQ.
- moe_fused_iq.cu: DevInfo.q2n; dev_info() sets up Q2_0 gate/up when the switch is set; gu_covered() and native_supported()
  take the pair only when q2n is set (a Q2_0 gate with another down type is refused, as on Turing).
- tests/cuda/prefill_fused_iq_test.cpp: --unset-pf-fused leaves STRATA_PF_FUSED unset (the engine's default) and skips below
  sm_80; CMakeLists.txt: the ctest entry prefill_fused_iq_test_q2n runs the Q2_0 / Q2_0 pair with the switch alone (exit 77 =
  skipped without an sm_80+ card; CUDA branch only).
- docs/DETAILS.md: one section in the author's measured numbers (RTX 5080) and the A4000 caveat below.

Unset (the default), nothing changes: dev_info() reads the variable once and sets nothing up, gu_covered() and
native_supported() return what they returned. Read line by line against base: the Turing path (t75) and the HIP path are
untouched. Set, the prompt arithmetic changes (first-token KL against FP16 0.0110, MMQ 0.0112, the author's figure) and the
fused path's smaller buffers let --prefill auto pick a larger chunk, which is most of the author's 16K gain; the generated
text can change.

Not verified: an RTX A4000 (sm_86). The kernel was taken on every CUDA card in #2145 and then kept to Turing after an A4000
no longer matched the old output and read 4K prompts 4.5% slower; the PR does not repeat that run. Merge note: #3277 and #3779
change dev_info() and native_supported() in the same file.

Tests: prefill_fused_iq_test_q2n and prefill_fused_iq_test pass on an RTX A4000 (sm_86, synthetic data); on the Q2_0 / Q2_0 layer the
fused kernel is 1.76x / 1.30x / 1.24x the MMQ path at 2048 / 4864 / 8192 tokens with the same relative RMS against FP32. Builds on CUDA
sm_86 (Linux) and sm_120 (MSVC). Not covered: no model run; the engine-level A4000 check is the gate.
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.

2 participants