Genesis · data type audit · updated Oct 15, 2026

Data types on SM86

English version of the report. Spanish original: claude.ai artifact. Model: BlairQ/qwen3.8_27b_idiotSavant_sm_86.

The pipeline currently serving on two RTX 3090s, operation by operation, compared against the data type that performs best on Ampere. The model is qwen3.8_27b_idiotSavant_sm_86, with a rotated residual stream so that per-token int8 does not break quality, plus its drafter qwen3.8_27b_idiotSavant_sm_86_dflash2. It was updated on Sep 28: per-head Hadamard at the input of o_proj and out_proj, and the GDN gates in W4. Between Sep 27 and Sep 28 the decode step dropped 25–27% from code changes measured one by one (section Improvements). The proposals from the Sep 23 version are marked with what each one delivered. On Oct 10 we added the evaluation against the benchmarks in the Qwen3.8-27B model card (Against BF16), a sensitivity chart that separates weight error from activation error (why it works well) and what was fixed in the server (The server). Oct 11: HLE and LiveCodeBench re-run with a 200k-token cap, and PN175, which closes on the server side the reasoning the model leaves open. The weights are the Sep 28 ones.

Model
idiotSavant sm_86
Residual stream
rotated · Hadamard 1024
Linear weights
int4 g128 · rotated GPTQ
GEMM activations
int8 per token
o_proj / out_proj
online per-head Hadamard
lm_head
W4A8 · logits per rank
Attention KV
int8 per token-head
Prefill all-reduce
int8 per group of 64
Decode all-reduce
direct P2P · int8 g64
Drafter
DFlash2 W4A16 · K=8 · tree

The result in five numbers

KL on responses, served W4A80.0178noon: 0.0385 · −54% · on Sep 25: 0.0193
crest factor of residual-stream inputs70 → 2.7max/rms per token
decode step · 1 / 4 requests22.0 / 30.0 mson Sep 27: 29.4 / 40.9 ms · −25% / −27%
prefill at 28k / 57k tokens2368 / 2014tok/s · Oct 10; on Sep 28: 2590 / 2180 (see note)
drafter, accepted tokens per step5.6240 agent requests, 4 in parallel · Oct 3

KL(BF16 ‖ model) over the positions generated by the assistant, in 48 windows of real coding-agent traffic. On prompt text KL means nothing: the chat model predicts <|im_end|> at 99% in user turns and any perturbation flips it. Prefill with pp.py (cold prompt, max_tokens 1). On Sep 28 the Sep 25 checkpoint was compared against the current one at the same time: 2570 / 2180 vs 2590 / 2180 tok/s, the same. On Oct 10, with the same checkpoint and today's stack (dynamic prefill chunk, 8 GiB L2, PN174), it gave 2368 / 2014: −9% / −8%. The weights did not change, so it is the stack: still to be bisected, together with the −5% from Sep 25 to Sep 28. The decode step is the Sep 28 profiler measurement, not repeated.

Against BF16: the Qwen3.8-27B model card benchmarks

Every benchmark in the official card that can be run with an endpoint and a public dataset, against the served model (W4A8, int8 KV, DFlash2 tree, two 3090s). Official prompts and graders, the card's sampling (thinking, temperature 1.0, top_p 0.95, top_k 20), reasoning_effort=xhigh, one sample per item. Finished on Oct 10; HLE and LiveCodeBench were re-run with a 200k-token cap (the first run used 81,920).

BenchmarkidiotSavantQwen3.8-27b-bf16Δn
IFBenchInstruction following83.0 ±4.379.5+3.5300
GPQA DiamondScientific reasoning91.9 ±3.889.2+2.7198
ERQAEmbodied reasoning67.5 ±4.665.5+2.0400
LiveCodeBench v6Competitive programming90.8 ±5.090.3+0.5131
CharXiv (RQ)Scientific charts81.3 ±2.483.7−2.41000
RealWorldQAReal-world perception82.0 ±2.785.9−3.9765
MathVisionVisual math81.9 ±4.390.0−8.1304 (testmini)
HLEMultidisciplinary reasoning23.6 ±3.7 · 28.2 closing </think>30.8−7.2500 of 2500
BabyVisionFine visual reasoning46.1 ±5.065.7−19.6388

What it says

  1. Short reasoning: a tie. IFBench, GPQA and ERQA stay within their interval. The quantization does not show. With a 200k-token cap LiveCodeBench also ties (90.8 vs 90.3; with 81,920 it gave 85.5). The reason is in the per-layer sensitivity.
  2. Very long reasoning: it loses. HLE −7.2 with a 200k cap; −2.6 if the missing </think> is closed.
  3. Fine perception: it loses. BabyVision −19.6, mostly on finding the odd figure in a grid and on following trajectories. The errors are genuine and image resolution is not the cause (median 0.3 MP). The vision tower is BF16, but the language model that reads its tokens is int4.

The long-reasoning symptom

With the 81,920-token cap of the first run, 16% of HLE and 10 of 131 LiveCodeBench items were cut off while thinking. That cap was set by the suite (the card allows up to 262k): with 200k only 8 of 500 HLE items hit the cap and LiveCodeBench rises from 85.5 to 90.8. What remains is something else: 16% of HLE ends without emitting </think>, with the answer written inside the reasoning, and the parser discards it. Grading those answers anyway, HLE gives 28.2. The rate of unclosed reasoning grows with length, over 574 responses from IFBench, GPQA and HLE: 0% below 8k tokens, 1% between 8k and 16k, 8% between 16k and 32k, 12% between 32k and 64k, and 25% between 64k and 82k.

PN175 fixes it at serving time: if the turn ends without </think>, the server appends it and continues with the prompt plus the reasoning, and the model writes the answer. On those 81 questions it gets 23 right, vs 24 when grading the reasoning by hand: the client receives an answer instead of empty content.

This is not an even comparison. Reference model: the BF16 column is the number Qwen publishes for the official model. idiotSavant comes from orcarouter's abliterated BF16, so the difference mixes abliteration, quantization and the harness. Judge: HLE, CharXiv and BabyVision are graded by the same model, not GPT-4o as in the card. Samples: HLE is a fixed sample of 500 and MathVision is testmini. Suite: benchmarks/qwen38_card in genesis-vllm-patches.

What happened to each proposal from Sep 23

ProposalStatusWhat was doneMeasured result
Rotate the residual stream (R1)done Block Hadamard of size 1024 with random signs, baked offline into the weights, the embedding and the lm_head; the g of each RMSNorm is folded into the next linear. A8 error per linear <1% (previously 5–48%). Lower int4 weight error in all 64 layers (median −8%).
Online Hadamard before down_proj (R4)done PN148. First in torch (−5% prefill), then fused in Triton, then SK-23 in CUDA. No cost. Without it the W4A8 KL rises 15%.
The activation is born in int8done in down, o_proj, out_proj SK-23 (down_proj): SiLU·mul + Hadamard + int8 per token + global scale in one kernel. SK-25 (Sep 28, o_proj and out_proj): gate or gated norm + per-head Hadamard + int8. Marlin receives the int8 directly, without going through Python. SK-23: +1.4% prefill, +0.6% decode. SK-25: −3 kernels per layer (~10 µs per GDN layer). In qkv, in_proj and gate_up it is still open (norm that writes int8).
RedHat's recipe on orcarouterreplaced Our own GPTQ with Hessians from real traffic and the rotated residual stream. AWQ was dropped: the rotation undoes the smoothing and performs better. idiotSavant. Rebuildable from the BF16 with one command.
PN130 normalized per rowno longer needed GPTQ leaves all scales positive and without AWQ's range. PN130 stays for its kernel (int32 reduction, register cap). PN125 does nothing.
lm_head quantized with GPTQnot needed Stays bf16 in the checkpoint, with the final norm folded in; PN139 converts it to int4 at load time, and since Sep 28 it runs as W4A8. Rotated gives the same error as unrotated: 11.85% vs 11.79%. In W4A8, the same KL and the lm_head −40% with 4 requests.
Fold the rotation into the drafterdone W_c·Rᵀ in the fc; the shared embedding and lm_head are converted online by PN149. Per-step cost not measurable: 38.3 vs 38.4 steps/s.
Learn from productiondone Capture of 840 agent requests against the served model + LoRA AUF, starting from Inco's original DFlash2. Offline greedy 5.70 → 6.13. Served: 5.63 accepted tokens per step; 5.62 with one more epoch (Oct 3, another bench).
Integer prefill attentionopen Design: QKᵀ int8, softmax fp32, P·V fp16/acc16 (see Improvements, step 14).Still the big front: 12.8% of short prefill, 40% at 100k.
Parallelize _k_escribirdone Sep 27 The tape, the conv and the tree recurrence in PTX.Tape 34 → 2.2 µs per layer; step −7% with one request.

The silicon rule

On GA102 the accumulator type matters as much as the input type. An fp16 mma with an fp32 accumulator, the default in FlashInfer, cuBLAS and Triton, runs at half the rate of the same with an fp16 accumulator and at a quarter of int8. That gap matters where compute dominates (prefill). In decode memory dominates, and bytes are what count.

int8 activations are the condition for using the 4× row. The problem that came up is that with int8 per token the scale is set by each token's maximum, and in this model a single channel (3994, the massive activation) is 70 times larger than the rest. Rotation solves that without touching the kernel.

SM86 is not SM80: how to take advantage of it

Almost all attention and GEMM software (FlashAttention, FlashInfer, cuBLAS, the Triton and Marlin templates) is designed for the A100 (SM80). The 3090 (SM86, GA102) shares the instruction set and runs the same binary, but has different ratios between units. What is free on SM80 can cost double here, and vice versa. This is the record of those differences and how best to use them; rows are added as they are measured.

What changes, per SM and per cycle

ResourceA100 · SM803090 · SM86What it implies here
Tensor fp16, fp32 accumulator1024 FMA256 GeForce halves the fp32 accumulator rate. It is the default in FlashInfer, cuBLAS and Triton.
Tensor fp16, fp16 accumulator1024512 2× over the previous one here; on SM80 it makes no difference.
Tensor int8 (IMMA)20481024 4× over fp16/acc32 (on SM80 only 2×). The best deal on the chip.
Tensor int440962048 (measured 2× s8) Same fragment layout as s8. Quality is the limit, not the kernel.
FP8—— Only from SM89. Nothing that depends on fp8 (SageAttention2, FA3 fp8) applies.
FP32 (FFMA)64128 (measured) Twice the float per SM…
INT32 (IMAD, LOP3, SHF, PRMT) and FMNMX64, own path64, shared …because the second path is FP32 or INT32. Each integer op takes a slot from float (FFMA + IADD mixed: 46 pairs per cycle).
MUFU (exp, rsqrt)1616 (measured) The narrowest unit. Runs in parallel with the rest.
int32 → float (I2FP)not measured64 (measured) Cheap on SM86: no magic-number tricks needed.
Shared memoryup to 164 KBup to 100 KB (99 per block) SM80's large tiles with 3–4 stages do not fit; with head_dim 256 it is the first limit.
Threads per SM20481536 Two blocks of 1024 cannot coexist. Fewer warps to hide latency.
Registers per SM64K64K The same, but with fewer threads each warp costs more: 255 registers leave 8 warps per SM.
L240 MB6 MB Do not count on the KV or the weights staying in L2: all decode pays DRAM.
Memory1.5–2 TB/s HBM936 GB/s (~640 sustained with power cap) Decode is decided by bytes: int4 for weights, int8 for KV.
Asynchronycp.asynccp.async Neither has wgmma or TMA (those are SM90). The FA3/FA4 overlap does not exist: latency is hidden only by interleaving warps.
Link between cardsNVLinkPCIe (here, no bridge) The decode all-reduce is latency, not bandwidth: NCCL takes ~25 µs for 92 KB. Writing directly into the other card's memory over P2P (PN152) brings it down to ~13–15 µs, and in int8 it also wins with large messages.

SM86 measured with tests/proto/sm86_throughput.cu (Sep 27): results per cycle and SM with clock64. Tensor: the 3090 spec (71 / 142 / 284 / 568 dense TFLOPS or TOPS at 1.695 GHz over 82 SMs); int4 at 2× s8 is measured. A100: the board spec.

Rules for writing kernels here

  1. Every fp16 mma with an fp32 accumulator is a candidate. Moving it to int8 gives 4×, and to an fp16 accumulator gives 2× if the sum is short (within a tile) and is then folded into fp32. Where it pays: prefill attention, the GDN chunk, the lm_head with many rows.
  2. No integer ALU in the hot loop. It shares a path with float and runs at half width. Softmax and epilogues in float, with MUFU exp; integers only where they replace bytes (weights, KV).
  3. Registers decide occupancy, and occupancy hides latency. Without wgmma, overlap between mma, softmax and loads only comes from having ≥8 warps per SM. With head_dim 256: fp16 accumulators per tile, split dimensions across warps and recompute what is cheap (QKᵀ int8) rather than synchronizing through shared memory.
  4. Tiles for 100 KB and 1536 threads, not SM80's. Fewer cp.async stages and smaller tiles, and the grid tied to the hardware (warps per SM), not to the problem.
  5. Decode is bytes. With 6 MB of L2 and ~640 GB/s, a decode kernel is measured in % of the DRAM ceiling. Marlin is at 98–103%; SK-18h at ~44% (35% before moving to 8 warps per block).
  6. Few communications. Over PCIe each all-reduce costs ~25 µs even if it moves 92 KB: fusing or eliminating all-reduces is worth more than compressing them.
  7. Measure on SM86 and with a fixed clock. Figures from papers (H100, A100) do not transfer: the exp that weighs 50% on H100 is ~7% of the pair here; the fp32 that is free on A100 costs double here.

Where it is already applied and where it is missing

PlaceTodayRuleStatus
LinearsMarlin W4A8, IMMA s81, 5applied
down_projSK-23 writes int8 directly1, 2applied
Decode attentionSK-18h: integer softmax, 254 registers; since Sep 28 8 warps per block (K/V once), no 64-bit division in the output2, 3, 5partial (step 5)
Prefill attentionFlashInfer fp16/acc32 over dequantized KV1, 2, 3step 14
GDN prefill chunkfla, fp16/acc321no plan yet
lm_head with 36 rowsW4A8, IMMA s8 (previously W4A16, compute-bound); logits per rank without AllGather1, 6applied
o_proj / out_projSK-25 writes the rotated input in int81, 2applied
in_proj_a / in_proj_bW4 inside the in_proj_qkvz Marlin (previously cutlass fp16)1applied
DrafterUnquantized fp16 GEMM; Triton attention with 12 blocks1, 4step 10
Decode all-reduceDirect P2P, int8 per group of 64 (PN152)6applied
GDN tree tapePTX, blocks per row and v segment4applied

Why rotation was needed: the A8 error per linear

Relative error of each linear's output with the activation in per-token int8, on real samples held out from calibration. It is the Marlin W4A8 computation, with the TP=2 partitioning simulated.

Linearcrest factorunrotated
median / worst
Hadamard
K block
per-channel
smoothing
Worst layers unrotated
k_proj / v_proj5610–14% / 48%0.7–1.0%1.8–2.3%3, 7, 11, 15 (early attention)
in_proj_qkv (GDN)5812% / 24%0.8%3.4%6, 1, 2, 13
in_proj_z (GDN)587.9% / 17%0.5%2.1%—
gate / up325–7% / 12%0.7–0.9%1.7–2.0%—
down_proj275.9% / 11%0.8%3.4%54–61
o_proj / out_proj12–212.4–3.4% / 8%1.3%2.5–3.0%Since Sep 28 they are rotated online per head (256 / 128) inside SK-25: the served KL dropped 0.0193 → 0.0178.

Whole layer: the A8 error was 13.6% of what the layer adds (median), the same size as the int4 weight error (~12%). In the whole model, W4A8 doubled the KL of W4A16.

Per-layer sensitivity, with and without rotation

KL on responses when a single layer is quantized and the other 63 stay in BF16. Each layer was measured twice: with int4 weights and fp16 activations (W4A16) and with int8 activations (W4A8). So each bar can be split in two: what the int4 weights cost (W4A16) and what int8 activations add (W4A8 − W4A16). That split is what explains why the model works well.

int4 weights · sum over 64 layers0.0149 → 0.0121−19% · better in 54 of 64 layers
int8 activations · sum0.0232 → 0.0014−94% · was 61% of the error, now 10%
full W4A8 · sum0.0381 → 0.0136−64% · unrotated → rotated
W4A8 / W4A16 · median per layer2.25 → 1.06int8 becomes almost free
int4 weights (W4A16) what int8 activations add (W4A8 − W4A16) ▲ full attention (the rest are GatedDeltaNet) · same scale in both panels · KL ×1e-4

Why it works so well, in four readings

  1. Unrotated, what broke things was not the weights but int8. The orange on top (0.0232) is larger than the blue (0.0149) and piles up in early attention: layers 3, 7, 11 and 15 account for 19% of that whole penalty. It is channel 3994, which sets each token's int8 scale and crushes the other 5119.
  2. Rotated, the orange almost disappears. With a crest factor of 2.7 instead of 70, per-token int8 costs the same as fp16 (median 1.06×). In other words: idiotSavant computes in int8, at the 4× tensor-core rate, with the quality of a W4A16. The bars below are almost all blue.
  3. Rotation also improves the weights. The blue drops 19%: GPTQ quantizes a matrix better without outlier columns, because the Hadamard spreads them in the weights too.
  4. What remains is even and small. The 8 worst layers account for 21% of the total: there is no single layer to fix, which is why per-layer mixed precision pays little. In total, the served model reaches KL 0.0178 per token and the same top-1 in 95.6% of positions. That is on the order of the difference between two identical server starts (~0.019 measured in probes on Sep 14, with another model). A task decided with margin (multiple choice, a format constraint, a short answer) does not flip because of that, and that is why it ties with BF16 on IFBench, GPQA and ERQA.

Where it falls short, and what the chart says about it

There are two things the chart measures and two it does not. What it does measure:

What it does not measure:

The next step is to repeat this same chart on 32–80k-token trajectories. It is done by teacher-forcing the BF16, layer by layer from the SSD, with the tokens idiotSavant generated on HLE, without generating anything. That tells whether the profile stays even at long context or whether culprit layers appear. And, in the responses where idiotSavant did not close the reasoning, whether the BF16 did emit </think>.

Measured on Sep 25, before rotating o_proj/out_proj online (today's served model also has that per-head Hadamard, which lowered the served KL from 0.0193 to 0.0178). 4 windows of 1024 response tokens of real agent traffic, exact KL over the full vocabulary, torch. The sum of the single-layer KLs (0.0136) is close to the KL of the whole model in torch (0.0116): errors from different layers add up with almost no compounding.

What the build says, layer by layer

The script that rebuilds the model leaves a report for each layer (informe_idiotsavant.json, which ships with the model). These are its three readings. The crest factor and the local error are from the Sep 25 build; the Sep 28 update redid only o_proj, out_proj and in_proj_a/b (idiotsavant.py realinear), and the weights table already includes them.

The crest factor of residual-stream inputs

Max over rms per token, at the median of calibration tokens, for the attention input and the MLP input of each layer. Unrotated it starts at ~71 and decreases with depth; rotated it stays between 2.6 and 4.1 in all of them. Layer 0 is the exception at the attention input (6.7): the massive activation is only created by its MLP.

unrotated · attention unrotated · MLP rotated · attention and MLP (almost overlapping)

The local error of each served layer

Error of the layer's output already in its servable form (rotated int4 weights, folded norms, Hadamard in down_proj) on the samples held out from calibration, relative to what the layer adds to the residual stream. It is the weight error; the A8 error is measured separately. Median 10.3% in the GatedDeltaNet layers and 9.2% in the attention layers.

GatedDeltaNet full attention ▼ layer 7: channel 3994 with g = 0 (the Hessian is taken before the norm)

The local error guides but does not decide: its correlation with the KL each layer causes is 0.6. Layer 0 has the lowest error (0.9%) because what it adds to the residual stream is large compared to its error; layer 63 has 5.3% error and is the most sensitive in the model.

The int4 weight error, by linear type

LinearRolelayersmedianminimumworst
q_projreads the residual stream1612.8%12.4%13.1% (layer 55)
k_projreads the residual stream1612.8%12.4%13.1% (layer 55)
v_projreads the residual stream1612.8%12.4%13.1% (layer 55)
in_proj_qkvreads the residual stream4812.7%11.9%14.4% (layer 0)
in_proj_zreads the residual stream4812.7%11.9%14.4% (layer 0)
in_proj_b / in_proj_areads the residual stream (GDN gates, W4 since Sep 28)4812.8% / 12.7%11.8%14.5% (layer 0)
o_projwrites the residual stream + Hadamard 2561613.8%13.4%15.3% (layer 7)
out_projwrites the residual stream + Hadamard 1284813.9%12.8%15.1% (layer 60)
gate_projreads the residual stream6412.7%11.7%13.1% (layer 52)
up_projreads the residual stream6412.7%11.7%13.1% (layer 52)
down_projwrites + Hadamard 5126413.2%12.5%15.7% (layer 0)

Relative Frobenius error between the rotated weight and its quantization (symmetric int4 g128, GPTQ). It is even, because rotation spreads outliers in the weights too. The ones that write the residual stream are slightly higher, and layer 0 is the worst in qkv and down_proj. A single dead channel in the whole model: layer 7, MLP input, channel 3994. With the Hadamard at the input, the Frobenius error of o_proj/out_proj rises slightly (13.6/13.3 → 13.8/13.9%): GPTQ minimizes the Hessian-weighted error, not this one, and the model's KL goes down.

Whole-model quality

Modeltorch W4A16torch W4A8vLLM W4A8 (served)top-1
noon (AutoRound)0.01550.03000.03850.938
GPTQ unrotated0.01220.02890.03650.940
idiotSavant, research build (Sep 24)0.01040.01160.01940.956
idiotSavant rebuilt from the BF16 with the public script (Sep 25)——0.01930.956
idiotSavant updated: per-head Hadamard in o_proj/out_proj, GDN gates in W4 (Sep 28)——0.01780.956

KL(BF16 ‖ model), top-20, response positions only. Torch: 4 windows. vLLM: 48 windows, with DFlash2 and the tree enabled. The rebuilt model shares 98.6% of its int4 values with the research build. The Sep 28 update with the Hadamard in torch gave 0.0176; served (SK-25 fused) gives 0.0178. Drafter acceptance under real agent load does not change: 5.546 before and 5.553 after (40 requests, 4 in parallel, 2 server starts).

The pipeline, operation by operation

The data types are today's (Sep 28). The percentages come from the Sep 20 and 21 traces (with noon) and remain as a reference for relative weight; the current breakdown, with idiotSavant at 62k context, is further below. The green stripe marks the rows that changed since Sep 23.

OperationInput → compute → outputDecodePrefillVerdictWhy
Residual stream and norms
Rotated residual streambaked into the weights h·Rᵀ, Rᵀ = diag(signs)·H₁₀₂₄ 00aligned There is no kernel: the embedding is born rotated and the linears that write the residual stream output rotated values. RMSNorm(h·Rᵀ) = RMSNorm(h)·Rᵀ.
RMSNorm + residualTriton, inductor fp16 → fp32 → fp16, weight = 1 ~2.5%~2.0%aligned The (1+w) weight was folded into the next linear. In layer 7 there is a channel with g = 0 exactly: the Hessian is taken before g so that GPTQ compensates for it.
Linears
qkv, in_proj, gate/upMarlin W4A8 + PN130 int8 (n·Rᵀ) × int4 → IMMA s8 → fp16 44.4%49.4%aligned Same kernel and type as before, now on an input with crest factor ~2.7 instead of ~56: int8 stopped losing quality.
down_projPN148 · SK-23 → Marlin gate|up fp16 → SiLU·mul + H₅₁₂ + int8 (registers) → IMMA s8 incl.incl.aligned One kernel replaces three: reads gate|up once and writes int8 with the scale already multiplied. 578 µs vs 890 at 8192 tokens.
o_proj, out_projPN154 · SK-25 → Marlin gate / norm + H256 / H128 + int8 (registers) → IMMA s8 (R·W·H) incl.incl.aligned Since Sep 28 the input is also rotated, with a per-head Hadamard that SK-25 applies together with the gate (attention) or the RMSNormGated (GDN) and the int8. A8 error ~2–3% → ~1%; −3 kernels per layer.
lm_headPN139 W4A8 · PN153 int8 × int4 (W·diag(g)·Rᵀ) → IMMA s8 → candidates per rank 3.6%0.1%aligned W4A8 since Sep 28 (with 36 rows W4A16 was compute-bound). No logits AllGather: each rank sends its max, its gumbel winner and its top-k, and sampling yields the same token.
in_proj_a / in_proj_bGDN, PN155 int8 × int4, inside the in_proj_qkvz Marlin 1.3%0.3%aligned W4 since Sep 28: the cutlass fp16 + split-K in each GDN layer is gone. Marlin does not accept them alone (24 rows per rank): they go as two 64-row chunks zero-padded into in_proj_qkvz. KL does not get worse.
Between linears
Quantize the activation_per_token_quant_int8 fp16 → row amax → int8 + scale ~2.0%*~1.2%*margin In down_proj, o_proj and out_proj it no longer exists (SK-23, SK-25). It remains before qkv, in_proj and gate/up: it could be saved by fusing it with the RMSNorm (SK-20, step 7).
a_scales × factortriton_poi_fused_…_mul fp32[M] × scalar ~1.0%*—margin Same case: folded in SK-23 and SK-25, still separate in qkv, in_proj and gate/up.
SiLU · mul inside SK-23 00aligned No longer writes an intermediate fp16.
Attention (16 layers)
DecodePN131 · SK-18h int8 KV → integer QK and softmax → fp16 5.0%—margin Reads the int8 KV without dequantizing; q and k rotated per head. Type aligned, but at 62k it uses ~44% of the bandwidth: 1 block of 8 warps per SM (254 registers) and 1.7 waves with 880-token pages.
PrefillFlashInfer + sk18h_decuant int8 KV → fp16 → HMMA fp32 acc —12.8%margin The slowest type in the table, and it dequantizes the KV in every chunk. 22% at 42k, 40% at 100k.
GatedDeltaNet (48 layers)
Spec recurrencegdn_arbol.cu (PN122, PTX) fp16 → fp32 → fp16 state 4.0%—aligned Rank 1 per token: there is no GEMM to accelerate. In PTX with whole rows per warp: 19.7 → 11.7 µs per layer.
Rollback tapepn122_cinta.cu (PTX) fp16 → fp32 → fp32 tape 7.2%—aligned It was a single 33 µs block, 48 times per step; in PTX with blocks per row and segment: 2.2 µs.
Prefill chunkfla fp16 → HMMA fp32 acc —4.1%margin Candidate for int8 like the tree's closed form; numerically delicate.
TP communication
Decode all-reducePN152 · SK-24, P2P fp16 → int8 g64 → sum in rank order 18.3%1.3%aligned NCCL took 26.7 µs for 92 KB (latency). Now each card writes its partial into the other over P2P, with an epoch on the GPU (it goes into the CUDA graphs) and in int8: ~13–15 µs with 1 request, 26.7 vs 44.9 µs with 4. KL +0.0001.
Prefill all-reducePN120 · int8 all-gather fp16 → int8 g64 → fp16 sum —20.5%aligned At the link ceiling. The rotated residual stream also flattens the int8 input.
DFlash2 drafter (idiotSavant)
fcW4A16, rotation folded 5 × h·Rᵀ → W_c·Rᵀ → features in the original basis incl. in the stepaligned The features come out identical to those of an unrotated target: capture and trainer do not change.
Shared embeddingPN149 E·Rᵀ → H₁₀₂₄·signs → E not measurablealigned Online block Hadamard, without reading weights.
Shared lm_headPN149 (h / g_final)·Rᵀ → int4 lm_head not measurablealigned For the logits and for the tree's top-16. The candidate selector reads the unrotated state.

* Estimated: SK-23 removes 1 of the 4 quantization sites in each layer, so ~¾ of what was measured on Sep 21 remains (2.7% and 1.3%). Marlin types according to the vLLM 0.29.0 ScalarType: Marlin<int8, uint4b8, float16, float16> for the projections and <float16, uint4b8, float16, float16> for the lm_head.

The measured step, layer by layer and kernel by kernel

Two snapshots of the same stack (idiotSavant + DFlash2 + 8-node tree, production compose in an isolated instance, no outside traffic), with the torch profiler on both ranks: the Sep 25 baseline (traces is25_*) and the Sep 28 state (is27_v2u_*), after steps 1–9 of Improvements. Three loads: one request with 62k context (9 rows per step: the root and 8 nodes), four requests of ~15k (36 rows) and a cold prefill of 15.4k in chunks of 1760. Each step is split into regions using SK-23 as the anchor: it runs once per target layer and each layer ends at the all-reduce that follows its down_proj. Both ranks give the same result within a 0.1% difference, so TP imbalance does not show up here.

Decode · 1 request · 62k29.7 → 22.0 msper step · GPU busy 92% → 99%
Decode · 4 requests · ~15k41.2 → 30.0 msper step · GPU busy 95% → 99%
GDN / attention layer · 1 request300 → 220 / 554 → 472 µs±1% across layers
Prefill · 15.4k cold2,730 tok/swith profiler, Sep 25 (not re-measured this way)

Where the time goes

Sep 28 · 1
41% 20% 9% 4% 14% 10%
Sep 28 · 4
41% 17% 12% 6% 13% 9%
Sep 25 · 1
30% 18% 12% 12% 10% 8% 9%
Sep 25 · 4
26% 17% 19% 13% 10% 5% 10%
Sep 25 · pf
49% 12% 22% 4% 6% 7%
target GEMMattention all-reduceGDN drafterGPU idlerest

The target GEMMs are the floor and are already at the DRAM ceiling (gate_up reads 44.6 MB in 60.5 µs, 737 GB/s): on Sep 25 they were 30% of the step with one request and today 41%, because everything else shrank around them. What remains far from its ceiling is decode attention (20%, ~44% of bandwidth) and the drafter (14%). The Sep 28 bars come from summing kernels by category (the Sep 25 breakdown came from the region analyzer), so the comparison is approximate.

One layer, kernel by kernel (1 request, 9 rows), Sep 28

GDN · 220 µsTypeµsSep 25
Marlin W4A8 ×4in_proj_qkvz(+b/a), out_proj, gate_up, downint8 × int4143.2141.9
in_proj_a/bnow inside in_proj_qkvz—06.2
all-reduce ×2P2P int8 (PN152)fp16 → int830.273.1
tree recurrencegdn_arbol.cu, PTXfp16 → f3212.218.7
tree convarbol_conv.cu, PTXf32 → fp164.39.0
rollback tapepn122_cinta.cu, PTXf32 → f322.434.7
gated norm + H128 + int8SK-25f32 → int83.45.2
RMSNorm + residual ×2inductorfp16 → f32 → fp166.712.1
quantize ×2 + scale × global ×2qkvz and gate_upfp16 → int87.6
SK-23SiLU·mul + H512 + int8fp16 → int85.75.0
copies and other—4.46.6
Attention · 472 µsTypeµsSep 25
attentionsk18h_batch2, 8 warpsint8 IMMA224.5284.4
Marlin W4A8 ×4qkv, o_proj, gate_up, downint8 × int4139.2~148
merge partitionssk18h_union4integer31.830.0
all-reduce ×2P2P int8fp16 → int829.875.1
outputsk18h_salida, no 64-bit divisioninteger → fp1611.816.5
write KV + prepare qfp16 → int88.37.6
gate + H256 + int8SK-25fp16 → int83.2~5.4
norms, quantize, scale, SK-23, q/k + RoPE—23.1~21

Decode attention reads 63 MB of int8 KV per layer and rank (62k × 2 heads × 256 × K and V). On Sep 25 it did so in 284 µs (35% of what the power-capped card delivers) with two blocks of 4 warps that loaded the same page; with one block of 8 warps, 224 µs (~44%). What holds it back now: 254 registers (one block per SM) and 880-token pages, which at 62k give 142 blocks for 82 SMs (1.7 waves). With 4 requests, sk18h_salida went from 84 to 10 µs per layer: it was doing an emulated 64-bit integer division for every element.

Outside the layers

Region1 request
Sep 25 → Sep 28
4 requests
Sep 25 → Sep 28
What is inside today
lm_head + logits644 → 461 µs1,841 → 558 µs Marlin W4A8 (previously W4A16, which with 36 rows was compute-bound: 972 µs). No logits AllGather (220 and 844 µs): each rank sends its candidates (PN153), same token.
verification453 → 105 µs556 → 161 µs Gumbel sampling, rejection, compaction, mamba state. The GPU no longer waits for Python: the hidden sync and the GDN metadata are gone (PN151).
drafter: 5 layers3,306 → 2,627 µs3,992 → 3,266 µs fc and context projection in CUDA graphs (PN150). What remains is the Triton attention with 12 blocks on 82 SMs (~1 ms) and the unquantized fp16 GEMMs: step 10.
drafter: lm_head + top-k468 → 492 µs796 → 560 µs The full lm_head (248k) for 16 candidates per position; in W4A8 since Sep 28.
tree + step preparation964 → 183 µs489 → 199 µs The ~110 torch kernels became a few kernels and CUDA graphs; the GPU barely waits.

Prefill, one chunk of 1760 (Sep 25)

A chunk takes 575–650 ms. The GEMMs are half and run at 90% of int8 peak at the 810 MHz of the 220 W cap (gate_up: 314 GOP in 2.55 ms, 123 TOPS). The int8 all-reduce is 22%: two per layer of ~0.95 ms each, 9 MB over PCIe at ~10.5 GB/s, not overlapped with anything. Prefill attention (FlashInfer fp16 with f32 accumulator, half rate on GA102) is 11.6% at 15k, plus 0.5 ms per layer of idle GPU in its preparation (sort and unique on the host).

In mixed steps (one request in prefill while others decode), sk18h_decuant dequantizes the KV to fp16 before FlashInfer: 0.6–1.3 ms per attention layer, 10–20 ms per step. In a prefill alone it is 0.14 ms. This is the opencode pattern: subagents that come in while the main thread generates. Since Sep 27 the requests that decode in a mixed step go through SK-18h and only the prefill dequantizes its pages: −63% dequantization.

The drafter: qwen3.8_27b_idiotSavant_sm_86_dflash2

It is Inco's DFlash2 (5 layers, block of 8, top-16 selector for the tree) fine-tuned directly against the served idiotSavant, with no intermediate models, and quantized to W4A16 (1.2 GB). It cannot be rotated entirely: its k_proj/v_proj serves two streams with different norms, the target's features and the block's tokens. So it stays in its original basis and is converted only where it touches the target's basis.

How it was built

  1. Fold the target's rotation into the fc (W_c·Rᵀ for each 5120 chunk).
  2. Capture 840 coding-agent requests with tools, regenerated by the served model: top-16 labels and features from the served fc.
  3. LoRA r=64 on the 7 linears; 0.9 TV + 0.1 CE with AUF. Conv, fc, norms and selector stay frozen. The trainer sees the effective embedding and lm_head that PN149 sees, int4 included.
  4. Quantize with RTN W4A16 g128, which is deterministic: the fc used in the capture is the final drafter's.

What it delivers

accepted/stepdecode tok/s
offline greedy, before → after5.70 → 6.13—
served, 40-request bench5.63~216
reference: noon + its drafter5.67216
with the updated model (Sep 28), 40 requests, 4 in parallel5.55—
one more epoch, same recipe (Oct 3), 40 requests, 4 in parallel, 4 server starts5.57 → 5.62—

8-node tree, coder profile, 3 replicas, totals (the per-request median is misleading between models that generate different texts). Same speed as before, with a target that has half the error. On Sep 28 the Sep 25 checkpoint was compared against the updated one on the same bench (4 in parallel, 2 server starts): 5.546 vs 5.553. The drafter did not need re-tuning. On Oct 3 it was published in place with one more epoch: 5.57 → 5.62 served, and 6.05 → 6.12 on the first 20 requests (it wins in all 3 server starts); the offline greedy length did not move. Its own GEMMs and its attention are still step 10.

Where margin remains, and how much

The table was built from the Sep 25 profile. Each row is a change that does not touch the model's numbers; the saving is the measured time minus the kernel's ceiling, so it is an upper bound. The estimate was that, if everything worked out, the one-request step would go from 29.7 to ~19.6 ms and the four-request step from 41.2 to ~25.6 ms, and that in practice half of that would materialize. By Sep 28 levers 1 (partially), 2, 3, 4 and 6 had landed: 22.0 and 30.0 ms, more than the realistic expectation. The status column says what is left of each one.

LeverWhat was measured on Sep 25 · what remains1 request4 requestsPrefillStatus
Decode
1 · Decode attentionSK-18h: occupancy and output batch2 at 35% of DRAM with 4 warps per SM; output with 1-warp blocks (84 µs with 4 requests). Remaining: batch2 at ~44%, limited by 254 registers and 1.7 waves with 880-token pages −2.8 ms · 9%−4.1 ms · 10%—partial: −6% / −10%; SK-30 in A/B
2 · Decode all-reduceraw single-pass P2P NCCL LL: 26 µs for 92 KB, 61 µs for 369 KB, ×128 per step. Our own P2P kernel reached 11.4 GB/s −1.8 ms · 6%−4.6 ms · 11%—done: −6.1% / −9.7%
3 · GDN tree recurrence_k_escribir, _k_salidas, _k_spec_arbol The tape in 1 block that walks 8 tokens × 8 heads serially (34 µs for 33 KB); the conv in 5 blocks −2.2 ms · 7%−2.8 ms · 7%—done: −7.0% / −4.7%
4 · GPU idle between phasesverification, drafter, tree 2.0–2.5 ms per step waiting for Python; ~110 torch kernels of 1–3 µs in building the tree −1.5 ms · 5%−1.0 ms · 2.5%—done: −6.1% / −1.8%
5 · Draftersplit attention, W4A8, pruned vocab Attention with 12 blocks (1 ms); fp16 GEMM (0.3 ms); full lm_head for 16 candidates (0.4–0.7 ms) −1.0 ms · 3%−1.3 ms · 3%—partial: ~−5.5% / ~−3% (step 10)
6 · Logits without AllGatherlocal gumbel-max per rank The full logits are gathered to sample; the max of logit + gumbel noise can be taken per rank and only (value, index) gathered −0.2 ms · 1%−0.8 ms · 2%—done, with A8 lm_head: −0.8% / −3.7%
7 · Norm that writes int8SK-20 wired like SK-23 1.7–1.9 ms per step in 1–3 µs kernels. It already exists (PN135), measured with noon; with idiotSavant's folded norm the kernel simplifies: re-measure −2–4%−2–4%−2%open (step 7); already done in o_proj/out_proj
Prefill
8 · Overlap the all-reducechunk halves, P2P with 10% of SMs 22% of prefill, real transfer (9 MB at 10.3 GB/s, the same on both ranks) and nothing in parallel. PN136 already overlaps it via DMA; with noon it did not win, but that does not carry over to idiotSavant: re-measure ——0 to −15%open: PN136 inert on idiotSavant
9 · Integer prefill attentionSK-18 over the int8 KV FlashInfer fp16/f32 (half rate on GA102) + dequantizing the KV; 10–20 ms per mixed step —mixed−6% at 15k · −20% at 64kopen; mixed steps already −63%
10 · Attention preparationsort and unique on the host 0.5 ms per attention layer and chunk with the GPU stalled ——−1–3%open
Floor
Target GEMM and lm_head Decode at the DRAM ceiling (98–103%); prefill at 90% of int8 peak with the 220 W cap. They only move with fewer bits or without the cap (+19.5% prefill measured) 9.6 ms11.8 ms49%not code

Possible improvements, in order

A single ordering that mixes two kinds of change. Code changes give the same output, bit for bit. Layer changes alter how a linear is rotated or quantized, and they are now possible because the rotated residual stream has a crest factor of ~2.7 instead of ~56. Everything measured with noon is measured again on idiotSavant. The criterion: cheap and safe first, then group the changes that touch the same kernel, and the layer changes in batches, because each batch pays for a quality validation.

The KL values shown in each step are those from when that step was measured, almost all with the Sep 25 checkpoint (0.0193). The current one, since batch B on Sep 28, is 0.0178.

Decode step · 1 request · 62k29.36 → 21.99 ms−25.1% (Sep 27/28)
Decode step · 4 requests · ~15k40.94 → 30.00 ms−26.7% (Sep 27/28)
GPU idle between phases · 1 request1,719 → 3 µsper step
Drafter · Sep 28 · 1 / 4 requests~−5.5% / ~−3%sum of three separate A/B tests (PN124, PN157/158, PN159)
QualitybetterKL 0.0193 → 0.0178; same acceptance under real agent load
#StepType1 request4 requestsWhy it goes here
1Tape, conv and tree recurrence in PTXpn122_cinta.cu, arbol_conv.cu, gdn_arbol.cu done Sep 27−7.0%−4.7% Measured against that day's baseline. Tape 34 → 2.2 µs per layer, conv 9 → 3.9, recurrence 19.7 → 11.7 (the state went from being split by columns, with a cross-warp reduction through a barrier, to whole rows per warp), and without PN54's 3 copies of q/k/v. Tape and conv bit-exact; the recurrence sums in a different order: error against fp64 equal to Triton and drafter acceptance equal or better.
1bMixed stepsdecodes through SK-18h done Sep 27—dequantization −63% Measured. In a step with prefill, the requests that decode go through SK-18h and only the prefill dequantizes its pages. Prefill identical; decode with SK-18h's normal difference (0.4–1.8%).
2Re-measure PN135 and PN136on idiotSavant needs wiring—— PN135 was never wired: its +0.4% was from the isolated kernel. PN136 is inert: the MLP is replaced by PN148/SK-23 and the overlap never kicks in (same TTFT in 2×2 runs). Both get integrated into the SK-23 path.
3lm_head in W4A8 + logits without AllGatherPN153 · PN139_A8 done Sep 28−0.8%−3.7% Measured. Each rank sends per row its max, its gumbel-max winner (same noise: key = global index) and, if needed, its top-(k+16). From that a sparse tensor is built and vLLM's sampling runs unchanged: same token (18,020 rows verified, 0 different). That alone: −0.5% / −1.9%. The W4A8 lm_head adds the rest; it also speeds up the drafter's, which shares it (904 → 556 µs). KL 0.0193 unchanged; acceptance 4.40 vs 4.37 tok/step.
4Decode all-reduce over direct P2PPN152 · SK-24 done Sep 27/28−6.1%−9.7% Measured, bit-exact with NCCL (same text in the oracle). Each card writes its partial into the other's memory (raw memory via IPC), signals with an epoch flag and sums. Double buffer, no final barrier, epoch on the GPU: it goes inside the CUDA graphs. Ideas from SiFAR (H200/NVSwitch) translated to PCIe 4.0 x8 with P2P. 90 KB: 36.9 → 14.8 µs; step −4.9% / −6.4%. On top of that, int8 per group of 64 (i8): both cards sum the two quantized parts in rank order, so they end up identical; it wins at all sizes (36 rows 44.9 → 26.7 µs), step −1.3% / −3.5% more. KL on responses 0.0193 vs 0.0192 all-fp16.
5Decode attention + Hadamard in o_projSK-18h 8 warps · output without div64 · SK-25 done Sep 28−6.5%−10% Measured, and bit-exact in the attention (same text in the oracle). batch2 with 8 warps: one block per (sequence, head) loads K/V once for the 64 rows, instead of two blocks of 4 warps that loaded the same data (−5.7% / −6.0%). output did an emulated 64-bit division per element: with reciprocal and correction, 18 → 12 µs and 86 → 10 µs per layer. union4 with adaptive groups (4 requests 55 → 36 µs). Layer: o_proj = R·W·H256 (per-head Hadamard) and SK-25 does gate + Hadamard + int8 in a 2.8 µs kernel.
6Hadamard in out_projPN154 · SK-25 GDN mode done Sep 28with step 8: −10 µs per GDN layer Measured. out_proj = R·W·H128; SK-25 does RMSNormGated + per-head Hadamard + per-token int8 and Marlin takes it directly (the inductor norm, the quantization and the scale × global go away). Together with o_proj, the served KL drops 0.0193 → 0.0178: it was the A8 error of the only linears with an unrotated input.
5bSingle-pass decode attentionSK-30 · ROTV · GENESIS_PN131_SK30 A/B queued−2.8% *−1.1% * batch2 reads K twice (exact max, then weights) and processes one page per block. SK-30 does flash-decoding over the same int8 KV: Q·K with mma m16n8k32 s8, online fp32 softmax with exp2 and 128 key groups according to the real length, read on the GPU (valid inside the CUDA graph). P·V with HMMA fp16→fp32 runs at half rate on GA102: P' = p·vs in two bytes (hi/lo) and two int8 mma, at 4× the rate. Offline, fixed clock, per layer: 20k 165 → 106 µs, 62k 288 → 215. In the kernel, instruction issue dominates (13 regular instructions per tensor instruction); ldmatrix.x4 for K, P and V, mask only at the edge and no rescaling if the max does not change: −10% instructions (ncu 203 → 191 µs). Still missing: the variant with P' at a fixed scale (persistent integer accumulators). ROTV: V rotated along d when writing the KV; with outlier channels the error drops from 2.9% to 2.2%, after fixing a truncation bias (x>>4 leaves a fixed −½ LSB in V and Q, which grows with length). * First run on the server, with GPU1's clock stuck: not valid, to be repeated.
7Norm that writes int8SK-20 with g = 1, wired like SK-23 code−2–4%−2–4% Depending on step 2. With 5–7 done, every linear receives int8 from the previous kernel.
8in_proj_a/b in W4, inside in_proj_qkvzPN155 · a single Marlin done Sep 28with step 6: −2.1% (1 request) Measured. GPTQ W4 on the rotated attn_in Hessian; Marlin does not accept them alone (24 rows per rank), so they go as two 64-row chunks (zero-padded) of in_proj_qkvz. The cutlass fp16 + split-K goes away. KL does not get worse (it drops, with step 6). Under real agent load (40 requests with tools, 4 in parallel) drafter acceptance stays the same: 5.546 with the Sep 25 checkpoint vs 5.553 with the current one.
9Python to the GPUPN150 drafter in a CUDA graph · PN151 GDN metadata in one kernel · hidden sync done Sep 27−6.1%−1.8% Measured. PN150: the drafter's fc and context projection in CUDA graphs (−3.1% / −1.6%). PN151: the GDN metadata builder (~35 torch ops per group and step) in one kernel, verified over 3000 calls with no differences (−2.0% / −0.6%). And a hidden synchronization of our own: torch.tensor(lista, device=cuda) in the tree compaction kept the CPU blocked 19.8 of 26 ms per step. GPU idle in gaps with 1 request: 1,719 → 3 µs per step. Same text in the oracle.
10Drafter: split attentionPN124 Q3D · segments over the window done Sep 28−3.1%−0.8% Measured. The drafter's attention (5 layers, 9 queries per request, window of 2047) went through Triton's 2D path, ~140 µs per layer. The 3D path splits the KV into segments, but the window pruned inside each one and at 62k only one did any work. Now the segments cover only the window: 3.6× offline, drafter layers −35% / −19%.
10bDrafter in Marlin + P2P embeddingPN157 · PN158 done Sep 28−0.5%−0.4% Measured. The context K/V was dequantized to fp16 every step (69 µs): Marlin W4A16 with the same int4 values, 25 µs, and 52 MB less. The embedding all-reduce moves to PN152's P2P. The int8 conv costs 0.9% of acceptance: it stays off until distillation.
10cDrafter's trimmed vocabularyPN159 · 64k of 248k columns done Sep 28−2.0%−2.2% Measured. The drafter only proposes among the 64k most frequent tokens in the captures (99.3% of what is generated): columns of the GPTQ int4 taken before the repack, without requantizing or rotating. Acceptance 5.61 vs 5.61. With 32k it falls to 5.41. Phase 2 (NanoSpec/EvoSpec translated to SM86): a fixed 32k + a 512-row ring with the prompt's tokens, read via UVA from the host. First result: 32k + ring 5.61 vs 64k 5.66, at half the lm_head. Our own top-k (SK-29) costs ~3% and is being reviewed.
10dDrafter fusions and attentionPN160 · SK-32 · SK-31 A/B queued—— The drafter had none of the target's fusions. SK-32 combines conv + qkv/gate_up quantization, SiluAndMul + quantization and the scale × global: from 11 small kernels per layer to 4, bit-exact or ±1 LSB against vLLM. SK-31 does the drafter's attention in one pass, with Q·K in fp16 (with int8 Q the error was 1.1%). Still missing: combining the q/k norm, rope, FWHT and KV write.
10eDistill the drafter with the changesint8 conv · trimmed vocabulary layeracceptance Agent bench, 2 server starts: production 5.57 · one more epoch with the usual recipe 5.69 (+2%) · trained int8 conv + 32k 5.29 · trained int8 conv + 64k 5.50. Training the conv with QAT hurts; the frozen conv is queued. The offline greedy length said the opposite for the extra epoch: offline does not predict the server for small changes.
Prefill
11Unique pages once per stepcode prefill −1–3%Small; can go in at any time.
12Prefill int8 AR group 64 → 128/256layer prefill −1–2%Fewer scales; the lower crest factor allows it. Same KL as step 4.
13Overlap the prefill all-reducecode prefill 0 to −15%Only if step 2 shows that PN136 hides the transfer on idiotSavant.
14SageAttention-style prefill attentionQKᵀ int8 · softmax fp32 · P·V fp16/acc16code −10–15% at 60kOver the int8 KV without dequantizing. Triton, 1–2 weeks. Before that, the intermediate step: dequantize only the new pages.

Prefill attention: why softmax in float and not integer

SK-18's all-integer path does, for each (query, key) pair, the exp via shift and quadratic, the weight split into two bytes and two mma passes for P·V. That is ~15 operations on the INT32 pipe, which on GA102 is half as wide as the FP32 one. The hardware exp (MUFU) is a separate unit that runs in parallel with the tensor cores. With head_dim 256 the exp is not the bottleneck, as it is on H100 with 128: what dominates is the mma type.

Per pair, GA102, hdim 256FlashInfer todayProposal
QKᵀfp16/acc32 · 1int8 · 0.25
P·Vfp16/acc32 · 1fp16/acc16 · 0.5
exp (MUFU)0.060.06
rest of the softmaxI2F, max, FFMA, FADD, pack~0.055~0.055
Total (cycles per SM)~2~0.85
  1. QKᵀ int8, K read directly from the paged cache; the per-token scale applied to the score. Optional: subtract the mean of K (Sage, exact under softmax).
  2. Softmax fp32 with MUFU exp2; if it became the limit, FA4's FMA-emulated exp.
  3. Conditional rescaling (FA4): O is only rescaled if the max rises by more than 8 in log2. Exact.
  4. P·V fp16 with an fp16 accumulator per tile, added to an fp32 O in registers: 2× on GA102, without touching the mma accumulators between tiles.
  5. P in int8: no. With long context the small weights get lost (8 bits breaks at 22k).

Expected 1.6–2× in attention (Sage measures ~2× over FlashAttention2 on 3090/4090), and without dequantizing. Measured throughput and rules in SM86 is not SM80. The same applies to decode: SK-18h uses the integer softmax and sits at 254 registers and 4 warps per SM. Moving it to float is a variant of step 5. Literature: SageAttention (arXiv 2410.02367), SageAttention2 (2411.10958), FlashAttention-3 (2407.08608), FlashAttention-4 (2603.05451).

Quality validation batches

  1. A, steps 3 and 4 — done: they do not requantize weights. KL 0.0193 unchanged with the A8 lm_head; +0.0001 with the int8 all-reduce.
  2. B, steps 5, 6 and 8 — done (Sep 28): idiotsavant.py realinear requantized only o_proj, out_proj and in_proj_a/b with the saved Hessians (~2 s per layer). KL 0.0193 → 0.0178; acceptance unchanged.
  3. C, step 10: drafter acceptance; re-tune if it drops.

Code steps are verified bit for bit against the current path before measuring speed.

Cumulative: estimated upper bound and measured

Up to stepmax. 1 requestmax. 4 requestsmeasured 1 / 4
1−7%−7%−7.0% / −4.7%
3−8%−10%—
4−14%−21%—
6 (with 8 and 9)−24%−33%−25.1% / −26.7%
10−35%−42%—

Measured against the Sep 27 baseline (29.36 / 40.94 ms per step). The steps were not applied in order (step 9 came earlier), so comparisons are only made where the set matches. Prefill gains another −8–10% with steps 11, 12 and 14.

The server: one crash, three chained problems and the empty responses

During the benchmark run (Oct 10), the engine crashed once and could not recover on its own. It was three different things, one after another. The fourth row is from Oct 11 and is not a crash: responses that arrived empty.

What happenedCauseStatus
Xid 31 on TP113:06 An SM wrote to an unmapped address (FAULT_PDE, write), in a step with 8 tree decodes and the chunked prefill of a request with an image. It happened once in ~20 h of load and did not reproduce with the same items. Ruled out: the L2 Triton kernel, the P2P mailboxes (they go through the copy engine) and SK-24 (all its writes are bounded). open · GPU coredump enabled
EngineCore OOM17:55 vLLM's multimodal processor cache is 4 GiB and is duplicated (API server and EngineCore). With the 12 GiB L2 in /dev/shm, the 26 GB cgroup was left with ~5 GB of headroom, and thousands of distinct images filled it (EngineCore anon 1.4 → 4.8 GB). fixed · 1 GiB cache, 8 GiB L2
Restart loop17:56–17:58, and twice on Oct 6 In vLLM 0.29.0, TieringOffloadingSpec does not pass the barrier to the shared region, and the L2 file is never unlinked. If the engine dies, the GBs stay in /dev/shm and the restart fails with "Insufficient space". fixed · PN174
Empty responsesOct 11 In long reasoning the model sometimes emits end-of-turn without having closed </think>: everything stays as reasoning and the content arrives empty (16% of HLE). PN175 appends </think> and continues with the prompt plus what was generated, which is in the prefix cache: only the answer costs anything. It does not apply with tools, images, n>1 or logprobs. fixed · PN175

PN174 does the unlink from the scheduler, which is the last process to open the region (8 s after the workers). If it were done earlier, the scheduler would create another empty file and work on different memory. Verified: all three processes map the (deleted) file and speed did not change (~130 tok/s of prose with one request). The container went down from 21 to 18 GB.