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.
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.
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).
| Benchmark | idiotSavant | Qwen3.8-27b-bf16 | Δ | n |
|---|---|---|---|---|
| IFBenchInstruction following | 83.0 ±4.3 | 79.5 | +3.5 | 300 |
| GPQA DiamondScientific reasoning | 91.9 ±3.8 | 89.2 | +2.7 | 198 |
| ERQAEmbodied reasoning | 67.5 ±4.6 | 65.5 | +2.0 | 400 |
| LiveCodeBench v6Competitive programming | 90.8 ±5.0 | 90.3 | +0.5 | 131 |
| CharXiv (RQ)Scientific charts | 81.3 ±2.4 | 83.7 | −2.4 | 1000 |
| RealWorldQAReal-world perception | 82.0 ±2.7 | 85.9 | −3.9 | 765 |
| MathVisionVisual math | 81.9 ±4.3 | 90.0 | −8.1 | 304 (testmini) |
| HLEMultidisciplinary reasoning | 23.6 ±3.7 · 28.2 closing </think> | 30.8 | −7.2 | 500 of 2500 |
| BabyVisionFine visual reasoning | 46.1 ±5.0 | 65.7 | −19.6 | 388 |
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.
| Proposal | Status | What was done | Measured 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 int8 | done 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 orcarouter | replaced | 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 row | no 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 GPTQ | not 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 drafter | done | 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 production | done | 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 attention | open | 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_escribir | done Sep 27 | The tape, the conv and the tree recurrence in PTX. | Tape 34 → 2.2 µs per layer; step −7% with one request. |
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.
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.
| Resource | A100 · SM80 | 3090 · SM86 | What it implies here |
|---|---|---|---|
| Tensor fp16, fp32 accumulator | 1024 FMA | 256 | GeForce halves the fp32 accumulator rate. It is the default in FlashInfer, cuBLAS and Triton. |
| Tensor fp16, fp16 accumulator | 1024 | 512 | 2× over the previous one here; on SM80 it makes no difference. |
| Tensor int8 (IMMA) | 2048 | 1024 | 4× over fp16/acc32 (on SM80 only 2×). The best deal on the chip. |
| Tensor int4 | 4096 | 2048 (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) | 64 | 128 (measured) | Twice the float per SM… |
| INT32 (IMAD, LOP3, SHF, PRMT) and FMNMX | 64, own path | 64, 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) | 16 | 16 (measured) | The narrowest unit. Runs in parallel with the rest. |
| int32 → float (I2FP) | not measured | 64 (measured) | Cheap on SM86: no magic-number tricks needed. |
| Shared memory | up to 164 KB | up 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 SM | 2048 | 1536 | Two blocks of 1024 cannot coexist. Fewer warps to hide latency. |
| Registers per SM | 64K | 64K | The same, but with fewer threads each warp costs more: 255 registers leave 8 warps per SM. |
| L2 | 40 MB | 6 MB | Do not count on the KV or the weights staying in L2: all decode pays DRAM. |
| Memory | 1.5–2 TB/s HBM | 936 GB/s (~640 sustained with power cap) | Decode is decided by bytes: int4 for weights, int8 for KV. |
| Asynchrony | cp.async | cp.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 cards | NVLink | PCIe (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.
| Place | Today | Rule | Status |
|---|---|---|---|
| Linears | Marlin W4A8, IMMA s8 | 1, 5 | applied |
| down_proj | SK-23 writes int8 directly | 1, 2 | applied |
| Decode attention | SK-18h: integer softmax, 254 registers; since Sep 28 8 warps per block (K/V once), no 64-bit division in the output | 2, 3, 5 | partial (step 5) |
| Prefill attention | FlashInfer fp16/acc32 over dequantized KV | 1, 2, 3 | step 14 |
| GDN prefill chunk | fla, fp16/acc32 | 1 | no plan yet |
| lm_head with 36 rows | W4A8, IMMA s8 (previously W4A16, compute-bound); logits per rank without AllGather | 1, 6 | applied |
| o_proj / out_proj | SK-25 writes the rotated input in int8 | 1, 2 | applied |
| in_proj_a / in_proj_b | W4 inside the in_proj_qkvz Marlin (previously cutlass fp16) | 1 | applied |
| Drafter | Unquantized fp16 GEMM; Triton attention with 12 blocks | 1, 4 | step 10 |
| Decode all-reduce | Direct P2P, int8 per group of 64 (PN152) | 6 | applied |
| GDN tree tape | PTX, blocks per row and v segment | 4 | applied |
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.
| Linear | crest factor | unrotated median / worst | Hadamard K block | per-channel smoothing | Worst layers unrotated |
|---|---|---|---|---|---|
| k_proj / v_proj | 56 | 10–14% / 48% | 0.7–1.0% | 1.8–2.3% | 3, 7, 11, 15 (early attention) |
| in_proj_qkv (GDN) | 58 | 12% / 24% | 0.8% | 3.4% | 6, 1, 2, 13 |
| in_proj_z (GDN) | 58 | 7.9% / 17% | 0.5% | 2.1% | — |
| gate / up | 32 | 5–7% / 12% | 0.7–0.9% | 1.7–2.0% | — |
| down_proj | 27 | 5.9% / 11% | 0.8% | 3.4% | 54–61 |
| o_proj / out_proj | 12–21 | 2.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.
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.
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.
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.
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.
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.
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.
| Linear | Role | layers | median | minimum | worst |
|---|---|---|---|---|---|
| q_proj | reads the residual stream | 16 | 12.8% | 12.4% | 13.1% (layer 55) |
| k_proj | reads the residual stream | 16 | 12.8% | 12.4% | 13.1% (layer 55) |
| v_proj | reads the residual stream | 16 | 12.8% | 12.4% | 13.1% (layer 55) |
| in_proj_qkv | reads the residual stream | 48 | 12.7% | 11.9% | 14.4% (layer 0) |
| in_proj_z | reads the residual stream | 48 | 12.7% | 11.9% | 14.4% (layer 0) |
| in_proj_b / in_proj_a | reads the residual stream (GDN gates, W4 since Sep 28) | 48 | 12.8% / 12.7% | 11.8% | 14.5% (layer 0) |
| o_proj | writes the residual stream + Hadamard 256 | 16 | 13.8% | 13.4% | 15.3% (layer 7) |
| out_proj | writes the residual stream + Hadamard 128 | 48 | 13.9% | 12.8% | 15.1% (layer 60) |
| gate_proj | reads the residual stream | 64 | 12.7% | 11.7% | 13.1% (layer 52) |
| up_proj | reads the residual stream | 64 | 12.7% | 11.7% | 13.1% (layer 52) |
| down_proj | writes + Hadamard 512 | 64 | 13.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.
| Model | torch W4A16 | torch W4A8 | vLLM W4A8 (served) | top-1 |
|---|---|---|---|---|
| noon (AutoRound) | 0.0155 | 0.0300 | 0.0385 | 0.938 |
| GPTQ unrotated | 0.0122 | 0.0289 | 0.0365 | 0.940 |
| idiotSavant, research build (Sep 24) | 0.0104 | 0.0116 | 0.0194 | 0.956 |
| idiotSavant rebuilt from the BF16 with the public script (Sep 25) | — | — | 0.0193 | 0.956 |
| idiotSavant updated: per-head Hadamard in o_proj/out_proj, GDN gates in W4 (Sep 28) | — | — | 0.0178 | 0.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 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.
| Operation | Input → compute → output | Decode | Prefill | Verdict | Why |
|---|---|---|---|---|---|
| Residual stream and norms | |||||
| Rotated residual streambaked into the weights | h·Rᵀ, Rᵀ = diag(signs)·H₁₀₂₄ | 0 | 0 | aligned | 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 | 0 | 0 | aligned | 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 step | aligned | 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 measurable | aligned | Online block Hadamard, without reading weights. | |
| Shared lm_headPN149 | (h / g_final)·Rᵀ → int4 lm_head | not measurable | aligned | 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.
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.
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.
| GDN · 220 µs | Type | µs | Sep 25 |
|---|---|---|---|
| Marlin W4A8 ×4in_proj_qkvz(+b/a), out_proj, gate_up, down | int8 × int4 | 143.2 | 141.9 |
| in_proj_a/bnow inside in_proj_qkvz | — | 0 | 6.2 |
| all-reduce ×2P2P int8 (PN152) | fp16 → int8 | 30.2 | 73.1 |
| tree recurrencegdn_arbol.cu, PTX | fp16 → f32 | 12.2 | 18.7 |
| tree convarbol_conv.cu, PTX | f32 → fp16 | 4.3 | 9.0 |
| rollback tapepn122_cinta.cu, PTX | f32 → f32 | 2.4 | 34.7 |
| gated norm + H128 + int8SK-25 | f32 → int8 | 3.4 | 5.2 |
| RMSNorm + residual ×2inductor | fp16 → f32 → fp16 | 6.7 | 12.1 |
| quantize ×2 + scale × global ×2qkvz and gate_up | fp16 → int8 | 7.6 | |
| SK-23SiLU·mul + H512 + int8 | fp16 → int8 | 5.7 | 5.0 |
| copies and other | — | 4.4 | 6.6 |
| Attention · 472 µs | Type | µs | Sep 25 |
|---|---|---|---|
| attentionsk18h_batch2, 8 warps | int8 IMMA | 224.5 | 284.4 |
| Marlin W4A8 ×4qkv, o_proj, gate_up, down | int8 × int4 | 139.2 | ~148 |
| merge partitionssk18h_union4 | integer | 31.8 | 30.0 |
| all-reduce ×2P2P int8 | fp16 → int8 | 29.8 | 75.1 |
| outputsk18h_salida, no 64-bit division | integer → fp16 | 11.8 | 16.5 |
| write KV + prepare q | fp16 → int8 | 8.3 | 7.6 |
| gate + H256 + int8SK-25 | fp16 → int8 | 3.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.
| Region | 1 request Sep 25 → Sep 28 | 4 requests Sep 25 → Sep 28 | What is inside today |
|---|---|---|---|
| lm_head + logits | 644 → 461 µs | 1,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. |
| verification | 453 → 105 µs | 556 → 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 layers | 3,306 → 2,627 µs | 3,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-k | 468 → 492 µs | 796 → 560 µs | The full lm_head (248k) for 16 candidates per position; in W4A8 since Sep 28. |
| tree + step preparation | 964 → 183 µs | 489 → 199 µs | The ~110 torch kernels became a few kernels and CUDA graphs; the GPU barely waits. |
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.
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.
| accepted/step | decode tok/s | |
|---|---|---|
| offline greedy, before → after | 5.70 → 6.13 | — |
| served, 40-request bench | 5.63 | ~216 |
| reference: noon + its drafter | 5.67 | 216 |
| with the updated model (Sep 28), 40 requests, 4 in parallel | 5.55 | — |
| one more epoch, same recipe (Oct 3), 40 requests, 4 in parallel, 4 server starts | 5.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.
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.
| Lever | What was measured on Sep 25 · what remains | 1 request | 4 requests | Prefill | Status |
|---|---|---|---|---|---|
| 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 64k | open; 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 ms | 11.8 ms | 49% | not code |
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.
| # | Step | Type | 1 request | 4 requests | Why it goes here |
|---|---|---|---|---|---|
| 1 | Tape, 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. |
| 1b | Mixed 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%). |
| 2 | Re-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. |
| 3 | lm_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. |
| 4 | Decode 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. |
| 5 | Decode 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. |
| 6 | Hadamard in out_projPN154 · SK-25 GDN mode | done Sep 28 | with 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. | |
| 5b | Single-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. |
| 7 | Norm 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. |
| 8 | in_proj_a/b in W4, inside in_proj_qkvzPN155 · a single Marlin | done Sep 28 | with 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. | |
| 9 | Python 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. |
| 10 | Drafter: 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%. |
| 10b | Drafter 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. |
| 10c | Drafter'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. |
| 10d | Drafter 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. |
| 10e | Distill the drafter with the changesint8 conv · trimmed vocabulary | layer | acceptance | 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 | |||||
| 11 | Unique pages once per step | code | prefill −1–3% | Small; can go in at any time. | |
| 12 | Prefill int8 AR group 64 → 128/256 | layer | prefill −1–2% | Fewer scales; the lower crest factor allows it. Same KL as step 4. | |
| 13 | Overlap the prefill all-reduce | code | prefill 0 to −15% | Only if step 2 shows that PN136 hides the transfer on idiotSavant. | |
| 14 | SageAttention-style prefill attentionQKᵀ int8 · softmax fp32 · P·V fp16/acc16 | code | −10–15% at 60k | Over the int8 KV without dequantizing. Triton, 1–2 weeks. Before that, the intermediate step: dequantize only the new pages. | |
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 256 | FlashInfer today | Proposal |
|---|---|---|
| QKᵀ | fp16/acc32 · 1 | int8 · 0.25 |
| P·V | fp16/acc32 · 1 | fp16/acc16 · 0.5 |
| exp (MUFU) | 0.06 | 0.06 |
| rest of the softmaxI2F, max, FFMA, FADD, pack | ~0.055 | ~0.055 |
| Total (cycles per SM) | ~2 | ~0.85 |
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).
Code steps are verified bit for bit against the current path before measuring speed.
| Up to step | max. 1 request | max. 4 requests | measured 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.
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 happened | Cause | Status |
|---|---|---|
| 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.