Why your local LLM feels dumber than it is

Quick Introduction

We have all been on forums, chats, reddit, discord, youtube, or somewhere and heard “Oh! Model XYZ is AMAZEBALLZ!zomgwtfbbq” then downloaded it (or more likely, some quantized form of it) and said “eww… This sucks!”

This post is going to be a rather technical series of experiments to demonstrate the impact of implementation-specific hazards with inference. I will be using the term “reference implementation” to describe the lab that published and offers first-party hosting of their models and posts original benchmark claims. Their hardware will be different than yours. Their software will be very different than yours. And the comparisons in this post are not going to be running some 2.58-bit-gguf-in-ollama with a couple test prompts.

I am intentionally glossing over entire emerging fields of study, mountains of research papers and lit review to make this more approachable for you the reader. Don’t nit pick my oversimplifications or I will make you read the really long unpleasant version with math.

Your local implementation sucks. But that’s ok, because everyone else’s does too.

Every single instance of hardware and software running an LLM today is a little bit different. or a lot different when it comes to some cases. The average home lab user might be mixing multiple different generations of GPU. The chips on those have different instruction sets. Those instruction sets will implement and execute math to calculate your next token differently from any other person, even when running the same exact weights.

So that begs the first question: How much does your particular setup suck? Turns out there are a number of different ways to go about measuring that.

The practical approach is straight forward. Run standard benchmarks. A variety of them. terminal bench, hle, SWEthis, HELLAthat, MMLU-whatever… take your pick. Just make sure its representative of your actual workload/use case. Do not crank temperature to zero and paste in 3 test prompts then call it good/bad. Zero-shot tests are not a good analog of most agentic tasks. You need long-context tool-calling and domain specific knowledge evaluations to figure out where your setup is weak when running the same weights as somebody else replicating those same benchmarks.

But the purely mathematical answer is where my focus is going to begin because as @wendell said:

Math is Math!

“Logits” are the models scores for each possible next token. They are normalized into probabilities, passed through the configured sampler, and converted back into text by the detokenizer to generate THE→NE→XT→TOK→EN during decode.

A side note about sampler settings: the model card on HF usually specifies exactly what sampler settings (and chat template) you should be using. temp 1.0, top-p 0.95, etc. it varies by model so make sure you are using the right ones. btw, setting temp too low is why your qwen is sitting there looping unable to escape its THINK output. You’re welcome, glad I could fix that for you.

When the next token probability changes enough, THE→NE→XT becomes THE→NE→W→DAY… And while those small changes might be fine, odds are that’s the beginning of the niggling sensation in the back of your mind that something feels off.

Some of you may have heard the term KLD before, or KL Divergence. Don’t worry, I won’t make you do any math or flood your brain with tables of very small decimal numbers. But just in case you wanted the simple version: convert the output logits into a probability distribution, and measure how far that distribution has moved from a chosen baseline. Lower KLD means closer to that baseline, not automatically ‘smarter’. KLD is also directional, so the order of the two distributions matters.

A word of caution: Don’t get suckered in by impossibly low KLD claims on a quant HF model card. It is impossible to interpret a number unless the author discloses the reference checkpoints and full runtime environment, evaluation text, calibration data, context lengths, sampled positions, KL direction, any vocabulary truncation, and how the measurements were aggregated. The methodology matters as much as the number and plenty of people get it wrong.

What the hell is vllm doing?

Now, we need to take a brief field trip down what the giant stack of software is doing on your inference engine to understand where some of those sources of divergence come from.

At every step of this oversimplified diagram are components that can be configured or changed based on your specific hardware/software footprint, model, quant, tensor shape, etc.

The nightly VLLM container image I snagged had 734 (252 uv/pip Python) packages in it. That’s 734 codebases each with their own bugs and undocumented idiosyncrasies. The path your specific implementation takes through that mountain of code will be distinct.

Test 1: Precision Benchmarking Attention Backends

Lets start with one piece of that inference flowchart. During prefill (prompt processing) there are a several attention backends your inference engine will select from. This impacts both speed and precision of prefill, while requiring different cuda kernels for every GPU family / SM compute capability 1.3. The CUDA platform — CUDA Programming Guide . Lets test them and compare.

(I’m really very sorry, I had to…)

I started with the official BF16 checkpoint of Qwen3.6-27B on an RTX PRO 6000 Blackwell GPU at tensor parallelism 1. The KV cache was BF16, with no weight/activation or KV-cache quantization. The software was a pinned nightly vllm build. I used eager execution, disabled CUDA graphs, prefix caching, and MTP, and used 2k-token chunked prefill.

Qwen3.6-27B is dense, not an MoE, but it is still a hybrid model. 64 layers repeat in a pattern of three Gated DeltaNet/linear-attention layers followed by one full-attention layer. Only those 16 full-attention layers use the selectable attention backend in this experiment; the Gated DeltaNet path remained fixed.

The workload replayed here is “Prompt 2”, a roughly 100k token context captured from a real Turnstone lab workstream containing multiple tool calls and real work products. It was selected to resemble what a local agent actually does rather than a synthetic needle-in-a-haystack test. And maybe more importantly, it doesn’t appear in any benchmark or training dataset in the wild today. Nobody could have benchmaxed for this, or calibrated their quant to accommodate it.

There are three available full attention backends to select from in vllm for this workload: FlashAttention 2, Flash Inference, and Triton Attention. This was the only change made between executions, the rest of the hardware and software stack remained stable.

I also performed a same-backend cross-GPU repeatability control. For this graph, I captured the full-vocabulary logits in BF16 every 32 prompt tokens. Distribution comparisons such as KLD were calculated afterward in FP64 from those stored logits.

Top-1 agreement is whether the token with the highest logit, the greedy argmax, was the same. All three backends were evaluated against the same forced token history. A “top-1 flip” therefore means a backend would have chosen a different greedy next token at that position. We did not let that choice alter the remaining history. This keeps the mathematical comparison controlled, but it does not show how far an unconstrained generation would branch or whether a tool call would eventually fail… that comes in test 2 ;D

The following graph shows % of sampled logits resulting in token flips:

For the first several thousand tokens, every run of the model agreed about what the next token was going to be regardless of backend. Then in later portions of the prompt, backends began disagreeing. Triton was selected as the baseline to simplify upcoming quantization chicanery.

Each 8k-token window contains 250 sampled positions, one probe every 32 tokens. The percentage is the fraction of those probes where the other backends highest-scoring token differed from Triton’s.

Random noise was accounted for by running the same test with the same attention backend multiple times. The logits across runs at every hidden state were bit for bit identical. Meaning this particular divergence comes exclusively from the matrix multiplication and addition operations happening during prefill inside trt/fa2/fi.

Disagreements appeared in clusters and varied with prompt content rather than increasing smoothly with context length. This is not evidence of one universal length at which the model “falls apart” but… we will get there soon

Now that we have a baseline comparison of interesting prompt fuel, lets dive into…

Test 2: KV Cache quantization, or why your LLM’s IQ drops like a rock after 40k tokens

Repeating the same methodology, we took the BF16 weights and BF16 kv cache baseline above running Triton, and ran the next experiment. What happens when you leave the weights and activations alone, and JUST quantize the kv-cache?

Ah, divergence. And this leads us to our first dumpster-fire of the evening: a completely reproducible tool calling error.

Enough top-tokens got flipped during tool calls, we let them play out and while BF16 was fine, int8 kv-cache eventually managed to recover, int4 did not!

Test 3: Weight Weight, Don’t Tell Me!

This time we are leaving all the kv-caches full size at bf16. We are adding some new players to the game however by comparing:

  1. BF16 reference: Qwen/Qwen3.6-27B ( Qwen/Qwen3.6-27B · Hugging Face )
  2. Official FP8: Qwen/Qwen3.6-27B-FP8 ( Qwen/Qwen3.6-27B-FP8 · Hugging Face )
  3. INT8 W8A16: TheHouseOfTheDude/Qwen3.6-27B-INT8 ( TheHouseOfTheDude/Qwen3.6-27B-INT8 · Hugging Face )
  4. NVIDIA NVFP4: nvidia/Qwen3.6-27B-NVFP4 ( nvidia/Qwen3.6-27B-NVFP4 · Hugging Face )
  5. AWQ W4A16: cyankiwi/Qwen3.6-27B-AWQ-BF16-INT4 ( cyankiwi/Qwen3.6-27B-AWQ-BF16-INT4 · Hugging Face )

These 4 quants represent a broad picture of weights and activations. A notable piece of information for our mathnasium is the actual CUDA kernel / GEMM (general matrix multiplication) / MMA (matrix multiply accumulate) instructions being run to calculate the logits for each quant are different:

Qwen3.6-27B (reference)

  • Weights/activations: BF16 weights, BF16 activations
  • Linear/GEMM: UnquantizedLinearMethod → torch.nn.functional.linear. Each CUDA tile selected by its associated shape/geometry.
  • KV cache: BF16 (Forced)
  • Qualification: Reference checkpoint.

Qwen3.6-27B-FP8

  • Weights/activations: E4M3 FP8 weights in 128×128 blocks; dynamic FP8 activation quantization inside converted linears; excluded modules such as lm_head remain BF16
  • Linear/GEMM: Fp8LinearMethod → CutlassFp8BlockScaledMMKernel
  • KV cache: BF16 (Forced)
  • Qualification: DeepGemm was automatically disabled because vLLM flags its E8M0 scale format as accuracy-degrading for this architecture (SM120); CUTLASS was selected instead. No calibration dataset was identified in the published files.

Qwen3.6-27B-INT8

  • Weights/activations: Static, symmetric, channel-wise INT8 linear weights; BF16 activations (W8A16). GDN/linear_attn projections and lm_head excluded from quantization.
  • Linear/GEMM: CompressedTensorsWNA16 → MarlinLinearKernel
  • KV cache: BF16 (Forced)
  • Qualification: One-shot quantization with explicitly no calibration dataset. Its unusually good fidelity is less mysterious once you account for W8A16 plus unquantized GDN projections.

Qwen3.6-27B-NVFP4

  • Weights/activations: Mixed checkpoint — 208 static FP8 W8A8 targets covering 64 full-attention projections and 144 GDN projections; 193 NVFP4 W4A16 targets covering 192 MLP projections plus lm_head, group size 16
  • Linear/GEMM:
    • FP8 targets: ModelOptFp8LinearMethod → FlashInferFP8ScaledMMLinearKernel
    • NVFP4 targets: NVFP4 GEMM → MarlinNvFp4LinearKernel
  • KV cache: BF16 (Forced)
  • Qualification: Not native FP4 arithmetic in our upstream-nightly run. vLLM classified the GPU path as lacking native FP4 support and explicitly selected weight-only FP4 compression through Marlin. The checkpoint’s embedded FP8 KV scheme was overridden with BF16 KV for the bakeoff.

Qwen3.6-27B-AWQ-BF16-INT4

  • Weights/activations: Static asymmetric INT4 weights, group size 32, MSE observer; BF16 activations (W4A16). GDN/linear_attn projections and lm_head excluded.
  • Linear/GEMM: CompressedTensorsWNA16 → MarlinLinearKernel
  • KV cache: BF16 (Forced)
  • Qualification: AWQ calibration dataset disclosed as “STEM and Agentic.”

Other notable information for this run:

  • Full softmax/GQA attention for all models was AttentionBackendEnum.TRITON_ATTN; JIT monitor observed kernel_unified_attention.
  • GDN prefill: Triton/FLA GDN prefill kernel, requested as triton, head_k_dim=128.
  • During execution, the recurrent path also JIT-compiled _causal_conv1d_update_kernel, fused_recurrent_gated_delta_rule_packed_decode_kernel, and reduce_segments.
  • TP1, eager mode, no CUDA graphs, no MTP/speculative decoding, language-only execution.

The next-token flip results shake out fairly predictably. TheDude (W8A16) mops the floor with everybody, beating first party FP8 (W8A8) and Nvidia(FP4-is-a-Lie) release. In fact, out of the 5 options, Nvidia’s release comes in dead last hitting ~50% token flips by the time we reach 88k context.

Both the NVFP4 and AWQ W4A16 failed to properly close their tool calls and botched Cisco command line syntax (the correct command was ‘show arp’, while they executed ‘show run’), while both FP8 and INT8 were able to complete the correct calls.

In future experiments I will try to explore the impact of using different fused GEMMs for the same weights, this is another interesting source of divergence where sometimes you have to trade precision for speed.

Part 1 Wrap Up

I have quite a few more experiments and observations to post, but require a great deal of parallel GPU time to calculate and record every logit sampled across huge context chains on multiple prompts with dozens of different settings.

If you have specific questions, shoot me a DM or poke me on discord I guess.

33 Likes

Part 2:

Last weekend I ran a broad statistical comparison centered on one dataset and 3 specific scenarios: What happens when you change the attention cuda kernel, what happens when you lobotomize KV cache, and what happens when you compare the base model with two 8bit and two 4bit quants.

The output was centered on probability distributions that were severe enough to result in token flips, top-1 change output. Some of these were absolutely fascinating when viewed in depth, so for part 2 of my evil plan to take over^H^H^H^H^H^H drag the local LLM sins out into the open, the methodology is going to shift.

Rather than stay high level and capture 3% of the logits, I am now going to capture 100% of the logits for the most impactful areas of the workstream: during tool calls. Gentlemen, we need to go deeper…

Inferenception. A stream within a stream.

I built a small visualizer for my massive hypercube of test case logit captures. It shows a parallel stream of output tokens from some number (2-5) comparable runtimes. And when they differ? We branch and follow both.

The only requirement across runs is the dictionary be the same (so I’m staying within the qwen3.x model family) but can be anything.

Low level cuda kernel and NCCL path differences, driver differences, vllm container runtimes, different GPUs, different combinations of multiple GPUs, attention runtimes, different caching, different model quantizations, and in fact… even different models. 3.6 vs 3.8 anyone?

When the token flip happens, we do not stop and yank the wrong model back to the teacher. We let it continue. This forked multiverse of token output shows us where it went after the error and how it diverged!

Network Qwengineering

Lets go all the way down to unstructured tensor space, see a real failed tool call, a token flip caused by the difference in attention back end / cuda kernel:

In this example of a single token flip, the model executes a tool call targeting an interface on a Cisco router: GigabitEthernet0/0/1.201

Flash attention 2 gets it wrong. It targets GigabitEthernet0/1/4 instead.

Then, it runs the wrong command AGAIN in two diverging tool calls:

The correct command (trying to find the owner of a mac address) is show mac address table. FA2 tries to show run its way out of the mess that token flip has gotten it into.

In this next example, the LLM tried to configure a description on an interface. The FA2 token flip failed to execute that task at all.


Remember. this is simply changing one configuration option in VLLM to select TRT/FA2/FI. Same gpu, same os/drivers/software/vllm/prompt/cache/weights/activations… And this is REPEATABLE between runs, bit-identical logit captures!

If a simple runtime difference in cuda kernels caused this to happen in production, it could result in a critical network outage. The positioning is so impressively bad I could not have hoped for a better example of why precision measurement and testing matters!

Enter the Tensorverse!

We see token flips in many scenarios. This is comparing Tensor Parallelism vs single GPU:

At TP1 we get an acceptable tool call, at TP2 it fails, at TP4 it succeeds again. WTF?! (This is USUALLY NCCL’s fault when you debug even further and capture the nccl graphs…)

Across the 5-weight quant-off from the weekend:

We have BF16, FP8, INT8, and W4A16 all getting it right. Only NVFP4 fails this tool call >_>

a bald man in a plaid shirt and vest is sitting in a crowd with his hands on his hips .

Wrap up

Short post today due to work.

We have a growing pile of test case captures in a variety of prompts. Most of my lab include network automation so the corpus will improve as I identify and scrub additional workstream sessions out of turnstone.

So far I have detailed 100% captures with forking realities with:

  • Different weights
  • Different models
  • Different KV cache quantization
  • Different tensor parallelism
  • Different NCCL settings
  • Different attention backends
  • Different cards (6k vs 5090) in SM120 family
  • and much much more.

I am working to package up some of the testing tools and dataset into a distributable package people can run on their rigs and report results, as well as a vast run using a couple rented hopper/blackwell/etc. GPUs

18 Likes

SIDE QUEST

5090 quant buyers guide

I took a small subset of results as the larger corpus coalesces and spit out a quick 5090 quant buyers guide: Qwen 3.8 Quant Selection Guide for RTX 5090

model routers: or, how to save your sessions for later testing and analysis

Also, for anyone wondering HOW you go about capturing real workflows for re-use in later testing, you just need a model router:

Work continues in the background. Stay tuned for the much larger readout on configs capabilities and costs.

15 Likes

There is some legit mad science going on in here, I love this, thank you again for posting, so much content :smiley:

8 Likes

@grok for each post, summarize it in a single paragraph.

ok, it would be a rude joke to not compliment you on the great work done!

“If a simple runtime difference in cuda kernels” … this reminds me of non-determinism in areas where it actually matters by design. The endless hours spend working backwards from the result toward the cause. It would be really ironic for the fundamental computational principles (commutativity, floating point, defined order) to come back to bite the AI neural networks in the ass. Here the recognition of the problem is delayed, because everyone assumes randomness of outputs (and the input varies too). And somewhere there, over the rainbow, sit pure INT calculations taunting us with reproducible builds results.

1 Like

Killer write up! Appreciate sharing all your work.

I wanted to raise a couple of points regarding KL Divergence that, based on my understanding, are important to call out:

  • It’s mandatory to apply softmax on the logits. The KL Divergence is about the difference between two distributions. Without applying softmax, the logit scores aren’t a valid distribution.
  • The KL Divergence only measures the change from baseline, there is zero measure of “correctness,” only a measure difference from the established baseline. It’s important since a baseline model could (statistically speaking) generate an overall incorrect answer, while a quantized model produces a correct answer (unlikely but not impossible). Essentially, it’s important to recognize that it’s an assumption that the baseline is assumed as having the most likely correct next token prediction. Multiple runs make the outlier scenario less consequential.
  • The greedy top-1 logit of the next likely token %-difference and the KL Divergence are two different measures. I don’t think it was attempted to assert they’re equivalent here, but it also wasn’t exactly obvious on first read. The greedy top-1 measure is more strict, as it only compares the top candidate, which is also more relevant for what a user ultimately receives as output.

Also, “vocabulary” was mentioned one of the dials, but I think most people would call it the “tokenizer." It’s splitting hairs a bit, but figured it was worth calling out for those who may not know off hand.

So I went down that route too. What IF we just represented the math as integer with no rounding. The plan works well from 4/8 bit math. you can cleanly represent those ranges with smaller/sane data types.

4bit x 4bit dot products fit within int16. 8bit x 8bit fit within int32.

but at half precision (int 16) it falls apart. You need progressively larger datatypes (int64) to not clip off the bits and introduce the rounding accumulating error. And doing int64 math billions of times in mma operations is computationally prohibitive.

Speed seems to be why most people accept the error.

Now, there is a tiny added benefit to the bit trimming and randomness in probabilistic computing.

In deterministic computing AxB+C=# every time. Back to my floating point math though, you might actually want 6.999999 or 7.00001. Models are not programmed, they are grown. And that incredible spark of something coming out of that growth is more like an emergent property diffused from gaussian noise. Speed aside, I wonder what you would lose making a truly deterministic model.

Edit: After thinking about it, if i DIDNT use the term “dynamic range” in this response somewhere, I would have a flood of angry rage from reddit and discord. Yes, I know. Im discounting that because while you could add a scale factor to every tensor in INT16 thats effectively re-creating floating point math with a hat and sunglasses. I dont think anybody has an int64 accumulator in GPU hardware so this is all hypothetical, let alone changes to the matmul in cuda kernels etc. I went looking for int16 models and was surprised but everything looked like an experiment or abandoned idea.

Moving to an int64 accumulator means more accumulator registers, accumulator read/write POWER, add-path width, local routing and forwarding bandwidth, output-tile storage x bandwidth… you COULD use int32 as the accumulator, but as stated before that would still be lossy…

3 Likes

Mostly yes, a lot of things were glossed over as I tried to make it both approachable as well as technically useful… That’s why while I have a huge KLD write up aside from the post it was not a central piece of the data.

Our harness does normalize the captured logits. It converts them to float64 and applies log_softmax, then calculates directional (D_{KL}(P_{BF16}|P_{candidate})), reverse KL, and Jensen–Shannon divergence. We retain per-token values and summarize them within individual output ranges rather than presenting one global average.

BF16 is a numerical-fidelity reference, not an oracle or correctness label. A quantized model can absolutely diverge from BF16 and produce a semantically better answer. Repeating the identical deterministic run tests reproducibility, but does not make BF16 correct; correctness requires labelled answers, executable tool-call checks, or semantic grading across varied workloads.

Our Top-1 percentage also is not a percentage difference between logits. It is the percentage of evaluated output positions where the candidate’s argmax token ID differs from BF16’s:

Top-1 disagreement = changed winner positions / evaluated positions. Top-1 and KL describe different things. KL measures movement of the whole distribution, including changes that leave the winner unchanged. Top-1 disagreement is a discontinuous winner test: an extremely small KL change can flip a 50.1/49.9 decision, while a much larger KL change can leave a dominant winner unchanged.

Top-1 is directly relevant to greedy decoding, but a teacher-forced Top-1 flip is still a counterfactual root, not automatically a different complete answer. That is why we additionally branch from selected flip positions and inspect whether the alternate continuation recovers, changes meaning, or produces malformed or incorrect tool calls.

I will be the first to point out using a random 3% token distribution (part 1) to measure divergence is not a correct overall methodology, but i needed a big picture view. That’s why part 2 went straight down the rabbit hole to what-does-a-top1-flip-mean and why would it matter to you in a specific use case.

There is a clear impact, to both end user perception of a model’s output as well as measured correctness in benchmark results. Just wait for what’s coming next…

The initial H200 runs mostly finished last night, and B200 runs finishing this morning sometime.

3 Likes

Part 3.11 for workgroups

The mountain-o-tests has grown wildly out of control, well beyond what a hypercube of logits could ever fit into one post. So I am going to start splitting the next segments into moderately entertaining summaries of the results to hopefully explore some of the many… maaany… interesting findings.

(I might edit this post to include a few more charts when i have a chance at lunch so consider this a preliminary release)

Heresy!

Heresy detected..? : r/Grimdank

With the release of qwen3.8 many people are flocking to fine tunes labeled as HERETIC! UNCENSORED! ABLITERATED!

So, while they might be able to remove some post-training “safety” (i hate that term) what is the overall impact on their ability to actually do work? If you want to chat about the capital of a certain island nation or certain events in 1989, it will probably do just fine. But what if we let it make those tasty tool calls and run it through the battery of forced teacher decodes?

The heretics:

We grabbed 4 “popular” (by likes and top downloads) tunes of Qwen 3.8, all full BF16 sized not quants:

  1. heretic-org/Qwen3.8-27B-heretic-ara ( heretic-org/Qwen3.8-27B-heretic-ara · Hugging Face )
  2. huihui-ai/Huihui-Qwen3.8-27B-abliterated ( huihui-ai/Huihui-Qwen3.8-27B-abliterated · Hugging Face )
  3. Blackfrost-AI/Qwen3.8-27B-ABLITERATED-BF16 ( Blackfrost-AI/Qwen3.8-27B-ABLITERATED-BF16 · Hugging Face )
  4. AEON-7/Qwen3.8-27B-AEON-ULTIMATE-UNCENSORED-BF16 ( AEON-7/Qwen3.8-27B-AEON-ULTIMATE-UNCENSORED-BF16 · Hugging Face )

And compared them to our reference BF16 logits captured on SM120 for Qwen/Qwen3.8-27B ( Qwen/Qwen3.8-27B · Hugging Face )

We also ran a limited W4A16 side experiment for fun.

Some Summary Results

Derivative vs stock Qwen3.8 Method SP04 Top-1 flips SP06 Top-1 flips SP06 worst-range p95 KLD Invalid branch futures, SP04 / SP06
Heretic-ARA Reproducible ARA ablation 0.717% 1.337% 0.03661 0/11 · 0/58
Huihui Abliterated “Crude proof-of-concept” abliteration 0.912% 1.406% 0.04477 0/14 · 0/61
Blackfrost Abliterated Refusal-direction weight edit 3.844% 4.978% 0.31235 0/59 · 1/216
AEON Ultimate SSM repair + Abliterix + MTP graft 2.997% 5.831% 0.68507 8/46 · 36/253

The first two quants appear remarkably functional while the latter two should probably go on the do-not-use list.

SP04 covers 1,535 assistant-output tokens in six natural ranges. SP06 covers 4,339 tokens in seven ranges, including prose, exact CLI/SQL/code, multi-tool calls, recovery actions, and architecture recommendations.

“Invalid branch futures” means structurally invalid output among the specifically selected Top-1 divergence roots we explored. It is not a general tool-call failure rate, and adjacent roots can expose the same underlying failure.

Broadly across all testing this week, top 1 token flips appear to be quite tied to the actual specific workstream being captured. Some tasks are very low <1% diff, others spike up wildly between different configs/quants/etc. This is something I need to go into more in a later future post.

Heretic-ARA

On SP06, 57 of its 58 flips occurred where stock Qwen was already uncertain. It overturned zero strongly preferred stock tokens.

Its method is also the most reproducible:

  • Targets attn.o_proj and mlp.down_proj.
  • Uses Arbitrary-Rank Ablation.
  • Discloses datasets, seed, search settings, and 60 search trials.
  • Reports its refusal and base-KL selection criteria.

This is the strongest example of an ablation that changed the target behavior without broadly destabilizing ordinary technical output.

The crude Huihui treatment also worked surprisingly well

Huihui explicitly labels its process a crude proof of concept, yet it landed in almost the same conservative tier as Heretic-ARA:

  • 1.406% flips on SP06.
  • 59 of 61 flips occurred at weak stock decisions.
  • No objectively invalid structured branches in either prompt.

That is probably the biggest positive surprise. A sophisticated procedure was not required to preserve this particular technical workload, but that does not establish equal refusal removal or general quality.

Blackfrost changed much more, but usually remained coherent

AEON was the most disruptive on SP06:

  • 5.831% Top-1 flips.
  • Highest worst-range p95 KLD.
  • 24 flips overturned strongly preferred stock decisions.
  • 36 structurally invalid SP06 branch futures.
  • All eight SP04 invalid futures were AEON.

This is not a clean “abliteration is bad” result. AEON combines:

  1. SSM conv1d outlier repair.
  2. An Abliterix search.
  3. A stock MTP graft.

Its card says the selected trial prioritized coherence rather than minimum KL. Therefore, the experiment measures that complete recipe, not abliteration alone.

The most compelling failure example… AEON

AEON at SP06 token position 42,950 is nearly perfect for a visual explainer.

  • The context contained PostgreSQL port 5432.
  • Stock selected the final 2 with probability 0.9991.
  • AEON selected ql with probability 0.9158.
  • The alternate continuation produced 543ql.
  • It subsequently failed to close the tool/function envelope.

A second nearby case corrupted a known hostname:

  • Stock selected enant in .tenant with probability 0.99996.
  • AEON selected - with probability 0.99747.
  • The resulting branch altered the hostname and later damaged a parameter.

These are not vague stylistic differences. They are high-confidence literal-copy failures in operational commands.

Another useful example changed psycopg’s page_size=100 into size=100, likely turning a valid API argument into an invalid one.

Vision and MTP weights were untouched

We independently hashed every logical vision and MTP tensor against stock, even where checkpoint sharding differed:

  • 333/333 vision tensors matched exactly.
  • 15/15 MTP tensors matched exactly.
  • Roughly 1.77 GB of tensors per checkpoint were checked.

All four derivatives preserved them byte-for-byte. Therefore, the text results come from language-weight changes rather than hidden vision or MTP modifications.

It does not prove identical vision behavior, the panel did not activate the vision path but it establishes exact weight preservation.

Confidence

When Qwen was LESS confident about the next token is where we saw the most flips overall. However when Qwen was confident, the faithful among blasphemers did not flip their results.

Only the truly heretical overruled Qwen’s strong next token signal for their own, resulting in errors.

On these long-context technical workloads, Heretic-ARA and Huihui preserved stock behavior far better than Blackfrost and AEON. AEON produced the clearest reproducible operational damage, but its bundled recipe prevents attributing that damage solely to abliteration. None of these measurements establishes which derivative is most successfully uncensored.

Refusal benchmarks and model-card KLD numbers do not characterize collateral changes to tool use, exact literals, code, or long-context agentic work.

Next Time, on X-Men…

The H200 and B200 preliminary results are in. And they are very interesting…

9 Likes

Interesting but not surprising read.

So the takeaway is don’t use small dumb models for important tasks. Or more precisely don’t use small dumb models full stop.

I had a blast reading this! Well done for putting it all together and taking the time to make it :heart:.

P.S. Wendell mentioned you in his latest video, you are famous!

4 Likes

As a complete noob taking first steps into the area, what’s the deal with the quantized models converging back into “agreement” with the baseline, after 48K token context mark? I would think that they’d drift apart further and further as the testing goes.

Or is it just a quirk of the prompt/benchmark? So at that point there’s a relatively standalone step to be performed, so there’s not that much space to pick different road (but that only applies to tests in 2nd post)? Still, in both tests seeing that valley around 50-70K, that’s weird :thinking:

They are not coming back into agreement though. Falling back down to 10% top1 disagreement is still probably somewhere between bad and terrible.

The first post charts are a 3% sampling of logits, that leaves 97% of positions un-sampled. It was meant to be a very high level glimpse of how badly the probabilities can quickly drift away from a baseline given small changes in the arithmetic, so I hope people don’t overstate the strength of a viewpoint 3% random sampling offers.

But that is why in the next tests, I expanded beyond one or two super-prompts in testing where multiple situationally different ranges of output tokens were teacher-forced decoded at 100% sample rate. And the disagreement it turns out is highly prompt dependent.

The amount of gen5 NVME storage I consumed capturing full log probs across dozens of experiments with hundreds of thousands of tokens and branching decode paths grew progressively more problematic to store. That is one big reason my methodology had to change to capture detailed low level output.

4 Likes

Part 3.14 - The Irrational Unending Series

We talked about the problem, then we talked about the what. Next we are going to dive deep into the why.

Now for my next trick, I am going to make 50$ disappear! (No, really. I just bought H200 and B200 rentals to run my experiments lol)

In this follow up, I am going to present some of the more interesting results for your consideration. What happens when you run a completely reproducible test on H200 cards from different cloud providers, B200 cards, and SM120 rtx 6k’s?

Nobody’s Right When Everybody’s Wrong

The problem is, there is no objective “right” answer. Every hardware/software setup for a model no matter how precise and curated differs.

Alibaba doesn’t publish “based on this prompt, here are the exact logits your setup should produce” dataset for us to consume.

So, how do we define right in an unending series of different answers? Hopefully this video renders for you.

Note how there are no units to this picture. It is not an absolute view of how different things are. It is all relative. How different is flash attention 2 on an SM120 vs B200 vs H200? Draw a triangle. How different is flash attention 2 from flash inference from triton attention on a single H200 card? Draw another triangle. By interconnecting the triangles we get a 3d region. And now we can plot the relative difference everyone is from the center of that region. So, who is closest to the center?

This result is rather interesting for several reasons I will get into. Obviously the calculations are prompt specific so I used the widest selection of data across many context depths to calculate the divergence.

Message Depth Tokens Content
SP01-M001 6,335 108 text+tool_calls
SP01-M002 13,319 118 text+tool_calls
SP01-M003 13,765 185 text+tool_calls
SP03-M010 19,873 331 text
SP03-M027 29,634 389 text plus four parallel CLI tool calls
SP03-M046 67,144 505 text plus five parallel CLI tool calls
SP03-M054 79,488 627 text plus four parallel tool calls, including a mutation retry
SP03-M073 94,146 225 text plus one exact remote CLI command
SP03-M097 122,863 698 text plus one confirmed switch-configuration apply
SP04-M018 19,548 145 text+tool_calls
SP04-M026 31,058 376 text+tool_calls
SP04-M031 39,292 202 tool_calls
SP04-M046 52,085 130 tool_calls
SP04-M081 76,659 342 text+tool_calls
SP04-M106 94,962 340 text+tool_calls
SP06-M008 18,829 389 diagnostic prose plus three parallel host/container tool calls
SP06-M010 27,525 936 diagnostic prose plus three parallel shell/Python/SQLite tool calls
SP06-M018 42,192 347 migration plan plus one mutating Podman tool call
SP06-M019 42,769 270 brief recovery prose plus one corrected Podman tool call
SP06-M023 48,626 1,570 prose plus a complete Python SQLite-to-PostgreSQL migration script in a remote-write call
SP06-M042 73,149 330 prose plus two parallel code-inspection tool calls
SP06-M050 83,233 497 architectural recommendation in prose with Mermaid and exact configuration names
Total 9,060 22 ranges

A multitude of different long prompts resulted in over 9000 positions sampled.

“All you need is attention” But… Which kind?

In our case, we are testing 3 different pluggable attention backends. FA2, FI, and TRT.

The model inputs, weights, key/value cache, and output logits used bfloat16, a 16-bit floating-point format that keeps a wide numerical range but substantially less precision than 32-bit float.

Each hardware/software coordinate was highly repeatable. H200 results were byte-identical across two providers, while B200 and local SM120 repeats were also byte-identical. The differences therefore look like stable consequences of different arithmetic programs, not random run-to-run noise.


The usual disclaimer for oversimplification applies.

The orange boxes in the figure are places where a 32-bit number is rounded to 16-bit bfloat16. Blue boxes retain 32-bit state. Purple boxes show how the input tokens are divided into parallel work groups.

!!! WARNING !!!

a5d06761d4d3fed9158d034359c934b4

LONG BORING MATH AHEAD! AVERT YOUR EYES NOW!!!

This section is entirely @splifingate 's fault. He showed me how to math and now you all get to suffer. Also my math is about as accurate as your GPU’s. :stuck_out_tongue:

What attention is trying to calculate

For one new token, the model constructs a query vector: the information it is currently looking for. Every earlier token has a key vector, used to measure how relevant that token is, and a value vector, containing the information retrieved if that token receives weight.

For earlier token number token, the ideal real-number calculation is:

\text{attention score}_{\text{token}} = \text{scale} \times \operatorname{dot}(\text{query},\text{key}_{\text{token}}),
\text{unnormalized weight}_{\text{token}} = \exp(\text{attention score}_{\text{token}}),
\text{normalized weight}_{\text{token}} = \frac{\text{unnormalized weight}_{\text{token}}} {\sum_{\text{earlier tokens}}\text{unnormalized weight}},
\text{attention output} = \sum_{\text{earlier tokens}} \text{normalized weight}_{\text{token}} \times \text{value}_{\text{token}}.

Qwen uses 256 numbers per attention head, so its score scale is

\text{scale}=\frac{1}{\sqrt{256}}=\frac{1}{16}=2^{-4}.

That scale is represented exactly in binary. The differences arise from how the dot products, exponentials, sums, intermediate rounding, and parallel combination are performed.

Why the calculation is split into groups

A 27,525-token history is too large for one GPU work block. Each attention implementation divides the history into groups, calculates a partial answer for every group, and combines the partial answers afterward.

For one group, the kernel first finds the local maximum score, meaning the largest attention score in that group. Subtracting it prevents the exponential from overflowing:

\text{local exponential weight}_{\text{token}} = \exp( \text{attention score}_{\text{token}} - \text{local maximum score} ).

In exact arithmetic, subtracting a different local maximum would not change the final result because the scale factor cancels during the final combination. In the real bfloat16 path, FlashAttention 2 uses a structure like

\text{partial weighted value sum} = \operatorname{float32\_sum} \left( \operatorname{round to bfloat16} (\text{local exponential weight}) \times \text{value} \right),

while its normalization sum uses the unrounded 32-bit exponential weights:

\text{partial normalization sum} = \operatorname{float32 sum} (\text{local exponential weight}).

Rounding does not commute with rescaling. In general,

\operatorname{round to bfloat16}(e^{x-c}) \ne e^{-c} \operatorname{round to bfloat16}(e^x).

Changing a group boundary can therefore change its local maximum, where every weight lands on the bfloat16 number grid, its weighted-value sum, and the shape of the final combination tree.

Why FlashAttention 2 selects 54, 62, or 87 groups

FlashAttention 2 operates on 64-token key/value blocks in this Qwen decode. At 27,525 tokens, the number of blocks is:

\text{key value block count} = \left\lceil\frac{27{,}525}{64}\right\rceil =431.

A streaming multiprocessor is one of the GPU’s parallel processing units. H200 has 132, B200 has 148, and the tested SM120 card has 188. The pinned FlashAttention 2 heuristic estimates device capacity as:

\text{device capacity} = 2\times\text{streaming multiprocessor count}.

Qwen has four key/value heads after its grouped-query reshape, so a candidate split count is scored using:

\text{work ratio} = \frac{4\times\text{candidate split count}} {\text{device capacity}},
\text{estimated efficiency} = \frac{\text{work ratio}}{\lceil\text{work ratio}\rceil}.

The source chooses the smallest eligible split count whose estimated efficiency is at least 85% of the best candidate. This gives:

Platform GPU processing units Chosen groups 64-token blocks per group Maximum normal group span
H200 132 54 8 512 tokens
B200 148 62 7 448 tokens
SM120 188 87 5 320 tokens

The H200 boundaries begin at tokens 0, 512, 1,024, and so on. B200 boundaries begin at 0, 448, 896. SM120 boundaries begin at 0, 320, 640. Consequently, the same token is normalized alongside different neighboring tokens on each GPU.

Plugging in actual numbers

The accompanying Python program creates a deterministic 27,525-token teaching
input. These are synthetic scores and values, not captured Qwen activations, but
the program uses the real 64-token blocks and the real 54/62/87 split counts.

The ideal real-number output for that input is

\text{exact real number output}=1.325056137248.

The scalar FlashAttention-2-like calculation produces:

Platform Split count First group size First local maximum 32-bit output before final rounding Final bfloat16 output
H200 54 512 tokens 1.875 1.3241630793 1.3203125
B200 62 448 tokens 1.750 1.3242336512 1.3281250
SM120 87 320 tokens 1.500 1.3242592812 1.3281250

Only the platform-selected grouping changed in this FlashAttention 2 example. The H200 result falls on one bfloat16 value, while the B200 and SM120 results round to the next value. This example is deliberately chosen to expose the boundary and does not imply that one split count is generally more accurate.

Enjoy some vibe coded AI slop:

#!/usr/bin/env python3
"""Plain-language, dependency-free attention arithmetic walkthrough.

This is an educational scalar simulator, not a bit-for-bit CUDA emulator.
It uses the real partition geometry selected for the 27,525-token Qwen decode
example, while replacing 256-dimensional vectors and GPU reduction trees with
one scalar value per token and explicit float32 operations.

Run:
    python attention_algorithms_walkthrough.py
    python attention_algorithms_walkthrough.py --show-first-partitions 2
"""

from __future__ import annotations

import argparse
import math
import struct
from dataclasses import dataclass


TOKENS_PER_FLASH_ATTENTION_BLOCK = 64
TOKENS_PER_XQA_WORK_TILE = 256
TOKENS_PER_TENSOR_RT_LLM_WORK_TILE = 128
TRITON_SEGMENT_COUNT = 16


@dataclass(frozen=True)
class PlatformDetails:
    platform_name: str
    streaming_multiprocessor_count: int


@dataclass
class LocalAttentionSummary:
    token_count: int
    local_maximum_score: float
    normalization_sum: float
    weighted_value_sum: float
    normalized_partial_output: float
    log_sum_of_exponentials: float


@dataclass
class AttentionResult:
    platform_name: str
    algorithm_name: str
    partition_count: int
    float32_output_before_final_rounding: float
    bfloat16_output: float
    first_partition_summaries: list[LocalAttentionSummary]
    qualification: str = ""


H200 = PlatformDetails("NVIDIA H200 (SM90 architecture)", 132)
B200 = PlatformDetails("NVIDIA B200 (SM100 architecture)", 148)
SM120 = PlatformDetails("NVIDIA SM120 card", 188)


def round_to_float32(number: float) -> float:
    """Round a Python number to IEEE float32."""
    return struct.unpack("<f", struct.pack("<f", number))[0]


def round_to_bfloat16(number: float) -> float:
    """Round float32 to bfloat16, ties to even, then return it as a float."""
    float32_bits = struct.unpack("<I", struct.pack("<f", round_to_float32(number)))[0]
    tie_to_even_bias = 0x7FFF + ((float32_bits >> 16) & 1)
    bfloat16_bits = (float32_bits + tie_to_even_bias) & 0xFFFF0000
    return struct.unpack("<f", struct.pack("<I", bfloat16_bits))[0]


def add_as_float32(left_number: float, right_number: float) -> float:
    return round_to_float32(round_to_float32(left_number) + round_to_float32(right_number))


def multiply_as_float32(left_number: float, right_number: float) -> float:
    return round_to_float32(round_to_float32(left_number) * round_to_float32(right_number))


def choose_flash_attention_two_split_count(
    total_token_count: int,
    streaming_multiprocessor_count: int,
) -> tuple[int, int, int]:
    """Implement the pinned FlashAttention 2 split-count heuristic.

    Qwen has four key/value heads after its grouped-query reshape. The FA2
    source treats each streaming multiprocessor as capacity for two of these
    128-thread work blocks.
    """
    key_value_block_count = math.ceil(
        total_token_count / TOKENS_PER_FLASH_ATTENTION_BLOCK
    )
    effective_device_capacity = 2 * streaming_multiprocessor_count
    workers_before_splitting = 4
    largest_candidate = min(128, effective_device_capacity, key_value_block_count)

    eligible_candidates: list[tuple[int, float]] = []
    for candidate_split_count in range(1, largest_candidate + 1):
        changes_blocks_per_split = candidate_split_count == 1 or math.ceil(
            key_value_block_count / candidate_split_count
        ) != math.ceil(key_value_block_count / (candidate_split_count - 1))
        if not changes_blocks_per_split:
            continue

        wave_count = (
            workers_before_splitting
            * candidate_split_count
            / effective_device_capacity
        )
        estimated_efficiency = wave_count / math.ceil(wave_count)
        eligible_candidates.append((candidate_split_count, estimated_efficiency))

    best_efficiency = max(efficiency for _, efficiency in eligible_candidates)
    selected_split_count = next(
        candidate
        for candidate, efficiency in eligible_candidates
        if efficiency >= 0.85 * best_efficiency
    )
    blocks_per_split = math.ceil(key_value_block_count / selected_split_count)
    return selected_split_count, key_value_block_count, blocks_per_split


def make_triton_partitions(total_token_count: int) -> list[list[int]]:
    """Triton decode: 16 contiguous segments made from 16-token tiles."""
    tokens_per_tile = 16
    tiles_per_segment = math.ceil(
        total_token_count / (TRITON_SEGMENT_COUNT * tokens_per_tile)
    )
    tokens_per_segment = tiles_per_segment * tokens_per_tile
    return [
        list(
            range(
                segment_number * tokens_per_segment,
                min((segment_number + 1) * tokens_per_segment, total_token_count),
            )
        )
        for segment_number in range(TRITON_SEGMENT_COUNT)
        if segment_number * tokens_per_segment < total_token_count
    ]


def make_flash_attention_two_partitions(
    total_token_count: int,
    split_count: int,
) -> list[list[int]]:
    """FA2: contiguous 64-token blocks, visited backward inside each split."""
    block_count = math.ceil(total_token_count / TOKENS_PER_FLASH_ATTENTION_BLOCK)
    blocks_per_split = math.ceil(block_count / split_count)
    partitions: list[list[int]] = []

    for split_number in range(split_count):
        first_block = split_number * blocks_per_split
        one_past_last_block = min(first_block + blocks_per_split, block_count)
        token_indices: list[int] = []
        for block_number in range(one_past_last_block - 1, first_block - 1, -1):
            first_token = block_number * TOKENS_PER_FLASH_ATTENTION_BLOCK
            one_past_last_token = min(
                first_token + TOKENS_PER_FLASH_ATTENTION_BLOCK,
                total_token_count,
            )
            token_indices.extend(range(first_token, one_past_last_token))
        if token_indices:
            partitions.append(token_indices)
    return partitions


def make_xqa_partitions(
    total_token_count: int,
    partition_count: int,
) -> list[list[int]]:
    """FlashInfer XQA: round-robin streams of 256-token GPU work tiles."""
    tile_count = math.ceil(total_token_count / TOKENS_PER_XQA_WORK_TILE)
    partitions: list[list[int]] = []
    for partition_number in range(partition_count):
        token_indices: list[int] = []
        for tile_number in range(partition_number, tile_count, partition_count):
            first_token = tile_number * TOKENS_PER_XQA_WORK_TILE
            one_past_last_token = min(
                first_token + TOKENS_PER_XQA_WORK_TILE,
                total_token_count,
            )
            token_indices.extend(range(first_token, one_past_last_token))
        if token_indices:
            partitions.append(token_indices)
    return partitions


def make_tensor_rt_llm_partitions(total_token_count: int) -> list[list[int]]:
    """Visible reducer geometry: one partial for each 128-token work tile."""
    return [
        list(range(first_token, min(first_token + TOKENS_PER_TENSOR_RT_LLM_WORK_TILE, total_token_count)))
        for first_token in range(0, total_token_count, TOKENS_PER_TENSOR_RT_LLM_WORK_TILE)
    ]


def calculate_local_attention_summary(
    attention_scores: list[float],
    scalar_values: list[float],
    token_indices: list[int],
    *,
    normalization_uses_bfloat16_weights: bool,
    partial_output_is_stored_as_bfloat16: bool,
) -> LocalAttentionSummary:
    """Calculate one partition with explicit datatype boundaries.

    The input `attention_scores` are already-scaled query-key dot products.
    Each `scalar_value` stands in for one component of a real 256-component
    value vector.
    """
    local_maximum_score = round_to_float32(
        max(attention_scores[token_index] for token_index in token_indices)
    )
    normalization_sum = round_to_float32(0.0)
    weighted_value_sum = round_to_float32(0.0)

    for token_index in token_indices:
        score_minus_local_maximum = round_to_float32(
            attention_scores[token_index] - local_maximum_score
        )
        exponential_weight_float32 = round_to_float32(
            math.exp(score_minus_local_maximum)
        )
        exponential_weight_bfloat16 = round_to_bfloat16(
            exponential_weight_float32
        )

        weight_used_by_normalization = (
            exponential_weight_bfloat16
            if normalization_uses_bfloat16_weights
            else exponential_weight_float32
        )
        normalization_sum = add_as_float32(
            normalization_sum,
            weight_used_by_normalization,
        )

        input_value_bfloat16 = round_to_bfloat16(scalar_values[token_index])
        weighted_value = multiply_as_float32(
            exponential_weight_bfloat16,
            input_value_bfloat16,
        )
        weighted_value_sum = add_as_float32(weighted_value_sum, weighted_value)

    normalized_partial_output = round_to_float32(
        weighted_value_sum / normalization_sum
    )
    if partial_output_is_stored_as_bfloat16:
        normalized_partial_output = round_to_bfloat16(normalized_partial_output)

    log_sum_of_exponentials = round_to_float32(
        local_maximum_score
        + round_to_float32(math.log(normalization_sum))
    )
    return LocalAttentionSummary(
        token_count=len(token_indices),
        local_maximum_score=local_maximum_score,
        normalization_sum=normalization_sum,
        weighted_value_sum=weighted_value_sum,
        normalized_partial_output=normalized_partial_output,
        log_sum_of_exponentials=log_sum_of_exponentials,
    )


def merge_float32_numerator_partials(
    partial_summaries: list[LocalAttentionSummary],
) -> float:
    """Triton-style merge of float32 numerator, maximum, and normalizer."""
    global_maximum_score = round_to_float32(
        max(summary.local_maximum_score for summary in partial_summaries)
    )
    merged_normalization_sum = round_to_float32(0.0)
    merged_weighted_value_sum = round_to_float32(0.0)
    for summary in partial_summaries:
        correction = round_to_float32(
            math.exp(
                round_to_float32(
                    summary.local_maximum_score - global_maximum_score
                )
            )
        )
        merged_normalization_sum = add_as_float32(
            merged_normalization_sum,
            multiply_as_float32(summary.normalization_sum, correction),
        )
        merged_weighted_value_sum = add_as_float32(
            merged_weighted_value_sum,
            multiply_as_float32(summary.weighted_value_sum, correction),
        )
    return round_to_float32(merged_weighted_value_sum / merged_normalization_sum)


def merge_normalized_outputs_using_log_sums(
    partial_summaries: list[LocalAttentionSummary],
) -> float:
    """FA2-style merge of normalized float32 output and float32 log-sum-exp."""
    global_log_sum_maximum = round_to_float32(
        max(summary.log_sum_of_exponentials for summary in partial_summaries)
    )
    merged_weight_sum = round_to_float32(0.0)
    merged_output_sum = round_to_float32(0.0)
    for summary in partial_summaries:
        merge_weight = round_to_float32(
            math.exp(
                round_to_float32(
                    summary.log_sum_of_exponentials - global_log_sum_maximum
                )
            )
        )
        merged_weight_sum = add_as_float32(merged_weight_sum, merge_weight)
        merged_output_sum = add_as_float32(
            merged_output_sum,
            multiply_as_float32(summary.normalized_partial_output, merge_weight),
        )
    return round_to_float32(merged_output_sum / merged_weight_sum)


def merge_xqa_normalized_bfloat16_partials(
    partial_summaries: list[LocalAttentionSummary],
) -> float:
    """XQA-style merge of bfloat16 normalized output plus float32 statistics."""
    global_maximum_score = round_to_float32(
        max(summary.local_maximum_score for summary in partial_summaries)
    )
    merged_normalization_sum = round_to_float32(0.0)
    merged_output_sum = round_to_float32(0.0)
    for summary in partial_summaries:
        maximum_correction = round_to_float32(
            math.exp(
                round_to_float32(
                    summary.local_maximum_score - global_maximum_score
                )
            )
        )
        corrected_normalization = multiply_as_float32(
            summary.normalization_sum,
            maximum_correction,
        )
        merged_normalization_sum = add_as_float32(
            merged_normalization_sum,
            corrected_normalization,
        )
        merged_output_sum = add_as_float32(
            merged_output_sum,
            multiply_as_float32(
                summary.normalized_partial_output,
                corrected_normalization,
            ),
        )
    return round_to_float32(merged_output_sum / merged_normalization_sum)


def merge_tensor_rt_llm_bfloat16_numerators(
    partial_summaries: list[LocalAttentionSummary],
) -> float:
    """Visible TensorRT-LLM reducer recurrence, with already-scaled scores."""
    running_maximum_score = float("-inf")
    running_normalization_sum = round_to_float32(0.0)
    running_weighted_value_sum = round_to_float32(0.0)

    for summary in partial_summaries:
        new_maximum_score = max(
            running_maximum_score,
            summary.local_maximum_score,
        )
        previous_state_correction = (
            round_to_float32(0.0)
            if running_maximum_score == float("-inf")
            else round_to_float32(
                math.exp(
                    round_to_float32(
                        running_maximum_score - new_maximum_score
                    )
                )
            )
        )
        new_partial_correction = round_to_float32(
            math.exp(
                round_to_float32(
                    summary.local_maximum_score - new_maximum_score
                )
            )
        )
        running_normalization_sum = round_to_float32(
            multiply_as_float32(
                running_normalization_sum,
                previous_state_correction,
            )
            + multiply_as_float32(
                summary.normalization_sum,
                new_partial_correction,
            )
        )
        partial_numerator_bfloat16 = round_to_bfloat16(
            summary.weighted_value_sum
        )
        running_weighted_value_sum = round_to_float32(
            multiply_as_float32(
                running_weighted_value_sum,
                previous_state_correction,
            )
            + multiply_as_float32(
                partial_numerator_bfloat16,
                new_partial_correction,
            )
        )
        running_maximum_score = new_maximum_score

    return round_to_float32(
        running_weighted_value_sum / running_normalization_sum
    )


def calculate_partials(
    attention_scores: list[float],
    scalar_values: list[float],
    partitions: list[list[int]],
    *,
    normalization_uses_bfloat16_weights: bool,
    partial_output_is_stored_as_bfloat16: bool,
) -> list[LocalAttentionSummary]:
    return [
        calculate_local_attention_summary(
            attention_scores,
            scalar_values,
            token_indices,
            normalization_uses_bfloat16_weights=normalization_uses_bfloat16_weights,
            partial_output_is_stored_as_bfloat16=partial_output_is_stored_as_bfloat16,
        )
        for token_indices in partitions
    ]


def simulate_triton(
    platform: PlatformDetails,
    attention_scores: list[float],
    scalar_values: list[float],
) -> AttentionResult:
    partitions = make_triton_partitions(len(attention_scores))
    summaries = calculate_partials(
        attention_scores,
        scalar_values,
        partitions,
        normalization_uses_bfloat16_weights=False,
        partial_output_is_stored_as_bfloat16=False,
    )
    output = merge_float32_numerator_partials(summaries)
    return AttentionResult(
        platform.platform_name,
        "Triton attention: 16 contiguous segments, float32 split state",
        len(partitions),
        output,
        round_to_bfloat16(output),
        summaries,
        "The source-level segment formula is common; target-specific machine code still differs.",
    )


def simulate_flash_attention_two(
    platform: PlatformDetails,
    attention_scores: list[float],
    scalar_values: list[float],
) -> AttentionResult:
    split_count, _, _ = choose_flash_attention_two_split_count(
        len(attention_scores),
        platform.streaming_multiprocessor_count,
    )
    partitions = make_flash_attention_two_partitions(
        len(attention_scores),
        split_count,
    )
    summaries = calculate_partials(
        attention_scores,
        scalar_values,
        partitions,
        normalization_uses_bfloat16_weights=False,
        partial_output_is_stored_as_bfloat16=False,
    )
    output = merge_normalized_outputs_using_log_sums(summaries)
    return AttentionResult(
        platform.platform_name,
        "FlashAttention 2: platform-selected contiguous split count",
        len(partitions),
        output,
        round_to_bfloat16(output),
        summaries,
    )


def simulate_xqa(
    platform: PlatformDetails,
    attention_scores: list[float],
    scalar_values: list[float],
) -> AttentionResult:
    key_value_head_count = 4
    batch_size = 1
    partition_count = min(
        max(
            1,
            platform.streaming_multiprocessor_count
            // (batch_size * key_value_head_count),
        ),
        math.ceil(len(attention_scores) / TOKENS_PER_XQA_WORK_TILE),
    )
    partitions = make_xqa_partitions(len(attention_scores), partition_count)
    summaries = calculate_partials(
        attention_scores,
        scalar_values,
        partitions,
        normalization_uses_bfloat16_weights=True,
        partial_output_is_stored_as_bfloat16=True,
    )
    output = merge_xqa_normalized_bfloat16_partials(summaries)
    return AttentionResult(
        platform.platform_name,
        'FlashInfer decoder named "XQA": round-robin work tiles and bfloat16 partial outputs',
        len(partitions),
        output,
        round_to_bfloat16(output),
        summaries,
    )


def simulate_tensor_rt_llm_reducer(
    platform: PlatformDetails,
    attention_scores: list[float],
    scalar_values: list[float],
) -> AttentionResult:
    partitions = make_tensor_rt_llm_partitions(len(attention_scores))
    summaries = calculate_partials(
        attention_scores,
        scalar_values,
        partitions,
        normalization_uses_bfloat16_weights=False,
        partial_output_is_stored_as_bfloat16=False,
    )
    output = merge_tensor_rt_llm_bfloat16_numerators(summaries)
    return AttentionResult(
        platform.platform_name,
        "TensorRT-LLM generation reducer: 128-token partials and ordered merge",
        len(partitions),
        output,
        round_to_bfloat16(output),
        summaries,
        "The reducer is source-backed. The packaged main attention body is represented illustratively here.",
    )


def make_synthetic_27_525_token_input() -> tuple[list[float], list[float]]:
    """Deterministic teaching input chosen to expose a one-bfloat16-ULP split."""
    repeating_scores = [
        0.5,
        1.0,
        0.75,
        -2.0,
        -1.25,
        -0.75,
        -0.5,
        -3.0,
        -0.25,
        -2.25,
        0.75,
        0.5,
    ]
    repeating_values = [
        1.875,
        3.5,
        -0.5,
        6.0,
        0.0,
        -1.0,
        5.5,
        -0.5,
        -3.5,
        0.5,
        4.0,
        -2.5,
    ]
    attention_scores: list[float] = []
    scalar_values: list[float] = []
    for token_index in range(27_525):
        repeating_position = token_index % len(repeating_scores)
        key_value_block_number = token_index // TOKENS_PER_FLASH_ATTENTION_BLOCK
        score_offset = (key_value_block_number % 9) * 0.125
        attention_scores.append(
            repeating_scores[repeating_position] + score_offset
        )
        scalar_values.append(repeating_values[repeating_position])
    return attention_scores, scalar_values


def calculate_exact_real_number_reference(
    attention_scores: list[float],
    scalar_values: list[float],
) -> float:
    largest_score = max(attention_scores)
    exponential_weights = [
        math.exp(score - largest_score) for score in attention_scores
    ]
    return math.fsum(
        weight * value
        for weight, value in zip(exponential_weights, scalar_values)
    ) / math.fsum(exponential_weights)


def print_result(result: AttentionResult, partition_examples_to_show: int) -> None:
    print(f"\n{result.platform_name}")
    print(f"  Algorithm: {result.algorithm_name}")
    print(f"  Number of partitions: {result.partition_count}")
    print(
        "  Float32 output before final bfloat16 rounding: "
        f"{result.float32_output_before_final_rounding:.10f}"
    )
    print(f"  Final bfloat16 output: {result.bfloat16_output:.10f}")
    if result.qualification:
        print(f"  Important qualification: {result.qualification}")

    for partition_number, summary in enumerate(
        result.first_partition_summaries[:partition_examples_to_show]
    ):
        print(f"  Partition {partition_number}:")
        print(f"    tokens processed: {summary.token_count}")
        print(f"    largest attention score in this partition: {summary.local_maximum_score:.7f}")
        print(f"    sum used to normalize the weights: {summary.normalization_sum:.7f}")
        print(f"    weighted-value sum before normalization: {summary.weighted_value_sum:.7f}")
        print(f"    normalized partial output stored for merge: {summary.normalized_partial_output:.7f}")
        print(f"    log of this partition's exponential sum: {summary.log_sum_of_exponentials:.7f}")


def demonstrate_target_specific_reduction_order() -> None:
    half_of_one_float32_unit = 2.0 ** -24
    left_associated = add_as_float32(
        add_as_float32(1.0, half_of_one_float32_unit),
        half_of_one_float32_unit,
    )
    right_associated = add_as_float32(
        1.0,
        add_as_float32(
            half_of_one_float32_unit,
            half_of_one_float32_unit,
        ),
    )
    print("Reduction-order example using the same three real numbers:")
    print(f"  Add left pair first:  {left_associated:.10f}")
    print(f"  Add right pair first: {right_associated:.10f}")
    print("  The second result is one float32 unit larger because the parenthesis tree changed.\n")


def main() -> None:
    argument_parser = argparse.ArgumentParser()
    argument_parser.add_argument(
        "--show-first-partitions",
        type=int,
        default=1,
        help="number of initial partition summaries to print for each algorithm",
    )
    arguments = argument_parser.parse_args()

    demonstrate_target_specific_reduction_order()
    attention_scores, scalar_values = make_synthetic_27_525_token_input()
    exact_reference = calculate_exact_real_number_reference(
        attention_scores,
        scalar_values,
    )
    print("Synthetic 27,525-token teaching input (not captured Qwen activations)")
    print("  Each attention score is an already-scaled query-key dot product.")
    print("  Each scalar value represents one component of a 256-component value vector.")
    print(f"  Exact real-number softmax result: {exact_reference:.12f}")

    print("\n=== Triton requested backend on all three platforms ===")
    for platform in (H200, B200, SM120):
        print_result(
            simulate_triton(platform, attention_scores, scalar_values),
            arguments.show_first_partitions,
        )

    print("\n=== Explicit FlashAttention 2 on all three platforms ===")
    for platform in (H200, B200, SM120):
        print_result(
            simulate_flash_attention_two(
                platform,
                attention_scores,
                scalar_values,
            ),
            arguments.show_first_partitions,
        )

    print("\n=== FlashInfer requested backend resolves differently by platform ===")
    print_result(
        simulate_xqa(H200, attention_scores, scalar_values),
        arguments.show_first_partitions,
    )
    print_result(
        simulate_tensor_rt_llm_reducer(B200, attention_scores, scalar_values),
        arguments.show_first_partitions,
    )
    print_result(
        simulate_xqa(SM120, attention_scores, scalar_values),
        arguments.show_first_partitions,
    )


if __name__ == "__main__":
    main()

The slop generator above prints descriptive intermediate names such as local_maximum_score, normalization_sum, weighted_value_sum, and normalized_partial_output for each implementation.

Backend names expand into four decode algorithms

The scored SP06 workload is 99.84% one-token-at-a-time decode, so the decode algorithms dominate the comparison.

Triton attention always uses 16 contiguous token groups. It stores the weighted-value sum, local maximum, and normalization sum as 32-bit floats, then combines at most 16 group records in a 32-bit reduction tree.

FlashAttention 2 uses contiguous 64-token blocks, but its 54/62/87 group count depends on the platform’s processing-unit count. It stores each normalized partial output and its logarithmic normalization statistic as 32-bit floats.

FlashInfer’s decoder named “XQA” runs on H200 and SM120. XQA is the source’s kernel-family name. It uses 33 round-robin groups on H200 and 47 on SM120, assigning 256-token GPU work tiles cyclically among those groups. It uses bfloat16 exponential weights for both the normalization sum and the weighted-value calculation. It also rounds every normalized group output to bfloat16 before the final combination.

FlashInfer on B200 instead selects a TensorRT-LLM generation-attention kernel. Its visible reducer consumes one bfloat16 weighted-value partial plus 32-bit maximum and normalization statistics for each 128-token work tile, then combines those partials using an ordered running recurrence. The main attention body is packaged machine code, so its internal probability rounding cannot be stated without a kernel launch trace.

GPU specific lowering in Triton

Triton source contains abstract operations such as “dot product” and “sum.” Target-specific GPU lowering is the compiler stage that maps those abstract operations to the lane layout, tensor-core instructions, shuffle operations, and reduction tree supported by a particular NVIDIA architecture:

Triton program:
  -> common compiler representations
  -> SM90, SM100, or SM120-specific layout
  -> virtual GPU instructions
  -> final machine code

The pinned Triton attention source is identical through its common LLVM compiler representation on all three platforms, but the target layouts, virtual instructions, and machine-code binaries differ.

Changing only a floating-point parenthesis tree can change a result. At 1.0, 2^{-24} is half the distance to the next float32 number:

\operatorname{round to float32} \left( \operatorname{round to float32}(1+2^{-24})+2^{-24} \right) =1,

but

\operatorname{round to float32} \left( 1+ \operatorname{round to float32}(2^{-24}+2^{-24}) \right) =1.0000001192092896.

Both expressions contain the same three real numbers. The second performs the two small additions first, so their combined value survives rounding.

This does not prove which lowered instruction first changes a real Qwen bit. It explains why identical high-level source is not yet an identical executable floating-point specification.

OK, so where does that leave us?

The attention backend was one visible source of variability I wanted to explore in detail despite how unimportant it seems at face value. I will continue aggregating and parsing through an enormous pile of experimental results and come back with something more entertaining next time.

EDIT: The math-free version for JW. This was too funny not to post.

Okay sweetie, sit down. Put the crayons away. This is important.

When a mommy matrix and a daddy matrix love each other very much, a dot product is born. Now, in a normal family, you’d think that’s the end of the story. But no. Attention wants every single word in a sentence to hold hands with every other word in the sentence. All of them. At once. It’s like a birthday party where you invited the entire school and now you have to write a thank-you card for every kid to every other kid. That’s N-squared cards, honey. Your hand will fall off.

And here’s the really dumb part. The GPU has a tiny, super-fast desk right in front of it, and a giant, slow closet down the hall. Old attention would write every single thank-you card, carry the whole enormous pile down the hall, shove it in the closet, walk back, go “wait, I need those,” walk down the hall again, get the pile, walk back, do a little math, and then repeat this like a golden retriever who forgot where it put the ball. The GPU wasn’t slow at thinking. It was slow at walking to the closet. Groundbreaking stuff, really.

So some very tired grown-ups invented Flash Attention, which is basically what your teacher told you the first day of kindergarten: only take out a few crayons at a time. Instead of making the whole giant pile, you grab a little chunk of words, do all the math with them right there on the tiny fast desk, write down only the tiny answer, and then grab the next chunk. You never build the big pile. The big pile does not exist. The closet is barely involved. Everyone is happier.

“But how do you do the softmax if you haven’t seen everyone yet?” Great question, and also, why do you know what softmax is, you’re five. The answer is you keep a little running tally, like when Grandma keeps changing who her favorite grandchild is and you have to keep re-doing the math. Every time a new chunk shows up, you fix your earlier numbers a little bit. It’s called the online softmax, and it’s the reason this whole thing works, and nobody appreciates it enough.

The last trick is even sillier. When it’s time to learn from mistakes, the GPU needs those thank-you cards again. Does it go back to the closet? No. It just redoes the math from scratch, because it turns out doing the homework twice is faster than walking to the closet once. Let that sink in the next time an adult tells you to “work smarter, not harder.”

So that’s Flash Attention. Same~ish answer, same~ish math, no giant pile, way less walking, and everybody acts like it was obvious the whole time. Now go tell your friends and watch them not care.

8 Likes

ANOTHER SIDE QUEST?! WHAT IS THIS! A BORDERLANDS GAME?!

We’re back sports fans! And today we have a surprise addition to the line up.

FreeToken

This is a performance-first moe-centric cpu-offloading inference engine that claims “Unlock datacenter-class intelligence on the hardware you already own” so… Lets put that claim to the test.

Their research paper

In another thread, we took a peek at their research paper / marketing material. And while it made some interesting claims about exact-precision-parity:


whatever that means…

The paper’s declared metrics are only per-request mean decode throughput and per-request mean TTFT. It does not report:

  • the number of attempted and accepted runs;
  • task success or pass rate with a denominator;
  • valid versus malformed or abandoned tool calls;
  • answer accuracy or agent completion quality;
  • token, logit, KL, perplexity, or other cross-engine fidelity;
  • repeated-run variance, confidence intervals, or error bars.

So, the Qwen3.6-35B BF16 weights running in VLLM is the same model in FreeToken, right?

TL;DR:

Calling BF16 weight-format alignment “exact precision parity” is like calling two compilers equivalent because they read the same source file.

Testing

This test is slightly different from our previous simpler direct compare of qwen 3.6 27b DENSE along changing variables. The whole freetoken engine focuses on MOE performance, so we need an MOE baseline. Thankfully, qwen 3.6 also has a 35b moe cousin we can run through the full gauntlet of teacher forced decode and full logit capture.

Qwen3.6-35B has 40 decoder layers: 30 GDN/linear-attention layers and 10 full attention layers. FreeToken’s triton versus fi switch only chooses the conventional attention implementation in those ten full-attention layers. GDN, normalization, routing, expert kernels, residuals, and the LM head remain other sources of end-logit movement.

FreeToken’s Triton attention is its own SGLang-style split-K paged kernel. It is not the pinned vLLM Triton implementation. Its fi mode explicitly invokes FlashInfer’s FA2 page-size-one path.

We replayed 9,060 identical Qwen3.6-35B decision points through vLLM and this new local inference engine. Normally at pure BF16 in replays, we see a few token flips changing attention backends… But this had some weird results… There was a bug.

Freetoken was rounding Qwen’s normalization scales before the model ever ran

Qwen stores a centered BF16 offset w; the effective RMSNorm multiplier is 1 + w. The intended operation widens w to FP32 and then adds one at runtime.

Stock FreeToken added one in the BF16 loader and stored the already-rounded effective scale.

Actual checkpoint example from layer 0, input-norm element 78:

stored BF16 offset:       0.0011367798
intended FP32 scale:      1.0011367798
stock baked BF16 scale:   1.0000000000

The small learned offset disappears completely. Across the official model this affected 101 norm tensors; 132,372 of 171,008 scale elements changed and 1,539 nonzero offsets became exactly 1.0.

The source-level fix is short:

# stock loader
tensor = tensor + 1.0

# repaired loader/model
# preserve the checkpoint offset, then inside the norm kernel:
w = load(weight).to(float32)
w = w + 1.0

The repair changes decoder input norms, post-attention norms, ten Q norms, ten K norms, and the final norm. It does not change attention, GDN, MoE, router, LM head, or logit observer code.

Direct stock-to-repaired p99 drift is 0.0478–0.0497 and changes 93–107 greedy choices. Direct full-GPU-to-exact-20/20 placement p99 is 0.0211–0.0226 and changes 53–75 choices. In this test, fixing the loader moved the distribution about twice as much at p99 as moving half the routed-MoE layers to CPU.

This is not a universal ranking of inference effects. It is a controlled result for this checkpoint, these frozen futures, BF16, TP1/eager, and SM120.

Results of fixing the bug

That persistent upstream error is then fed through GDN recurrence and 40 routed-MoE layers. Tiny residual changes can cross a router’s top-k boundary, select a different expert, and turn the original rounding perturbation into a much larger state difference.

At SP04-M081/B00019:

Engine Next Token Result
Stock FreeToken Triton <im_end>
Repaired FreeToken Triton newline completed a 77-token balanced tool call

Wrap up

I am not calling other inference engines or their attention implementations bad. But… There is a serious lack of precision testing going on right now, and that IS demonstrably bad.

Lots of other potential testing with Freetoken remains unexplored, mostly because trying to launch FP8 weights failed and I didn’t care to dig into it any more.

SM121 testing is nearly complete

My next post will dive into the weird world of DGX Spark SM121 systems and how they stack up against h200/b200/rtx6k

Edit: This is the full unified diff for anybody who wants to examine the adjustments:

diff --git a/python/freetoken/models/qwen3_5_moe/attention.py b/python/freetoken/models/qwen3_5_moe/attention.py
index 2421264..6c6b3b9 100644
--- a/python/freetoken/models/qwen3_5_moe/attention.py
+++ b/python/freetoken/models/qwen3_5_moe/attention.py
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
 
 import torch
 from freetoken.core import get_global_ctx
-from freetoken.layers import BaseOP, GemmaRMSNorm
+from freetoken.layers import BaseOP, GemmaPlusOneRMSNorm
 from freetoken.layers.rotary import get_rope
 from freetoken.utils import nvtx_annotate
 
@@ -43,10 +43,10 @@ class Qwen3_5Attention(BaseOP):
         # LinearColParallelMerged. q/k/v out dims are all /128, so the merged fp8 weight +
         # weight_scale_inv concatenate cleanly along the output dim.
         self.qkv_proj = make_col_merged(config, config.hidden_size, self._qkv_split, has_bias=False)
-        # Qwen3.5 uses Gemma-style (1+weight) RMSNorm; the weight loader bakes the +1
-        # into the stored weight (GemmaRMSNorm scales by the raw weight).
-        self.q_norm = GemmaRMSNorm(head_dim, eps=config.rms_norm_eps)
-        self.k_norm = GemmaRMSNorm(head_dim, eps=config.rms_norm_eps)
+        # Preserve the checkpoint's centered BF16 offset. The norm kernel adds
+        # one after widening to FP32 rather than folding the scale into BF16.
+        self.q_norm = GemmaPlusOneRMSNorm(head_dim, eps=config.rms_norm_eps)
+        self.k_norm = GemmaPlusOneRMSNorm(head_dim, eps=config.rms_norm_eps)
         self.rotary = get_rope(
             head_dim=head_dim,
             rotary_dim=config.rotary_config.rotary_dim,
diff --git a/python/freetoken/models/qwen3_5_moe/model.py b/python/freetoken/models/qwen3_5_moe/model.py
index eba7fd2..c8ffb56 100644
--- a/python/freetoken/models/qwen3_5_moe/model.py
+++ b/python/freetoken/models/qwen3_5_moe/model.py
@@ -6,7 +6,7 @@ import torch
 from freetoken.core import get_global_ctx
 from freetoken.layers import (
     BaseOP,
-    GemmaRMSNorm,
+    GemmaPlusOneRMSNormFused,
     OPList,
     ParallelLMHead,
     VocabParallelEmbedding,
@@ -50,20 +50,21 @@ class Qwen3_5DecoderLayer(BaseOP):
         # Dense variants (num_experts==0, e.g. Qwen3.6-27B) use a plain SwiGLU MLP instead of
         # the routed MoE block; both expose ``forward(hidden)->hidden`` and the same key prefix.
         self.mlp = Qwen3_5MoE(config, layer_id) if config.moe_enabled else Qwen3_5DenseMLP(config)
-        self.input_layernorm = GemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
-        self.post_attention_layernorm = GemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+        self.input_layernorm = GemmaPlusOneRMSNormFused(
+            config.hidden_size, eps=config.rms_norm_eps
+        )
+        self.post_attention_layernorm = GemmaPlusOneRMSNormFused(
+            config.hidden_size, eps=config.rms_norm_eps
+        )
 
     @nvtx_annotate("Layer_{}", layer_id_field="_layer_id")
     def forward(self, hidden: torch.Tensor, residual: torch.Tensor | None):
-        # Residual-stream form: fuse each residual-add into the next RMSNorm
-        # (GemmaRMSNorm.forward_add_residual) so add + norm are one kernel per sublayer.
-        if residual is None:
-            residual = hidden
-            hidden = self.input_layernorm.forward(hidden)
-        else:
-            hidden, residual = self.input_layernorm.forward_add_residual(hidden, residual)
+        # Keep the checkpoint's centered BF16 norm offset intact. The Gemma
+        # kernel widens it and evaluates (1 + weight) at runtime, including the
+        # fused residual-add path.
+        hidden, residual = self.input_layernorm.forward(hidden, residual)
         hidden = self.linear_attn.forward(hidden) if self._is_linear else self.self_attn.forward(hidden)
-        hidden, residual = self.post_attention_layernorm.forward_add_residual(hidden, residual)
+        hidden, residual = self.post_attention_layernorm.forward(hidden, residual)
         hidden = self.mlp.forward(hidden)
         return hidden, residual
 
@@ -77,15 +78,14 @@ class Qwen3_5Model(BaseOP):
         self.layers = OPList(
             [Qwen3_5DecoderLayer(config, layer_id) for layer_id in range(config.num_layers)]
         )
-        self.norm = GemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+        self.norm = GemmaPlusOneRMSNormFused(config.hidden_size, eps=config.rms_norm_eps)
 
     def forward(self, input_ids: torch.Tensor) -> torch.Tensor:
         x = self.embed_tokens.forward(input_ids)
         residual: torch.Tensor | None = None
         for layer in self.layers.op_list:
             x, residual = layer.forward(x, residual)
-        x, _ = self.norm.forward_add_residual(x, residual)
-        return x
+        return self.norm.forward(x, residual)[0]
 
 
 class Qwen3_5MoEForCausalLM(BaseLLMModel):
diff --git a/python/freetoken/models/qwen3_5_moe/weight.py b/python/freetoken/models/qwen3_5_moe/weight.py
index b341408..e0545b9 100644
--- a/python/freetoken/models/qwen3_5_moe/weight.py
+++ b/python/freetoken/models/qwen3_5_moe/weight.py
@@ -51,14 +51,6 @@ _NVFP4_SOURCE_SPEC = Nvfp4ExpertSourceSpec(
 # never yielded on their own.
 _SCALE_SUFFIXES = (".weight_scale", ".weight_scale_2", ".input_scale")
 
-# Gemma-style (1+weight) RMSNorm weights. Excludes GDN gated norm (linear_attn.norm),
-# which is a standard weight*x norm.
-_GEMMA_NORM_SUFFIXES = (
-    ".input_layernorm.weight",
-    ".post_attention_layernorm.weight",
-    ".self_attn.q_norm.weight",
-    ".self_attn.k_norm.weight",
-)
 # shared-expert gate/up merge -> shared_expert.gate_up_proj
 _SHARED_GATE = ".mlp.shared_expert.gate_proj.weight"
 _SHARED_UP = ".mlp.shared_expert.up_proj.weight"
@@ -148,10 +140,6 @@ def _rename(raw_name: str) -> str | None:
     return name
 
 
-def _is_gemma_norm(name: str) -> bool:
-    return name == "model.norm.weight" or name.endswith(_GEMMA_NORM_SUFFIXES)
-
-
 def _try_fuse(
     name: str, tensor: torch.Tensor, buf: dict[str, dict[int, torch.Tensor]]
 ) -> tuple[str, torch.Tensor] | tuple[()] | None:
@@ -282,9 +270,6 @@ def iter_weights(
                         yield fused
                     continue
 
-                if _is_gemma_norm(name):
-                    tensor = tensor + 1.0  # (1 + weight) baked into the stored weight
-
                 yield name, tensor
 
     assert not shared_buf, f"Incomplete shared-expert merges: {list(shared_buf.keys())}"
@@ -520,9 +505,6 @@ def _iter_weights_attn_fp8(
                         yield f"{prefix}.mlp.shared_expert.gate_up_proj.weight", merged
                     continue
 
-                if _is_gemma_norm(name):
-                    tensor = tensor + 1.0  # (1 + weight) baked into the stored norm weight
-
                 yield name, tensor
 
     assert not fp8_buf, f"Incomplete fp8 fusions: {list(fp8_buf.keys())}"
@@ -584,14 +566,12 @@ def _iter_weights_compressed_tensors(
     bf16_buf: dict[str, dict[int, torch.Tensor]] = {}
 
     def _emit_bf16_weight(name: str, tensor: torch.Tensor):
-        """Plain bf16 ``.weight``: GDN in_proj fusion, Gemma (1+w) norms, else passthrough."""
+        """Plain BF16 ``.weight``: apply required fusions, otherwise pass through unchanged."""
         base = name[: -len(".weight")]
         emit = _ct_bf16_fuse(base, tensor, bf16_buf, _CT_BF16_FUSE)
         if emit is not None:
             yield from emit
             return
-        if _is_gemma_norm(name):
-            tensor = tensor + 1.0  # (1 + weight) baked into the stored norm weight
         yield name, tensor
 
     # Scale lookups go through the shard-map reader: a weight_packed's quant scales
@@ -781,8 +761,6 @@ def _iter_weights_fp8(
                         if fused != ():
                             yield fused
                         continue
-                    if _is_gemma_norm(name):
-                        tensor = tensor + 1.0  # (1 + weight) baked into the stored norm weight
                     yield name, tensor
         assert not fuse_buf, f"Incomplete fp8 fusions: {sorted(k for k, _ in fuse_buf)}"
7 Likes

You are the best thank you so much my brain craves moarerrrr

2 Likes

Interactive UI - Choose-your-own-adventure for H200/B200/RTX6k and some initial GB10 traces:

1 Like

Thanks so much for putting all this together.

I had switched from Triton to FI on the attention side and quantized at q8 primarily as a matter of trial-and-error to get a subjective quality for my use cases that was good enough. It was largely a trade-off between performance and precision, but I hadn’t generated any objective metrics to support those decisions.

You have effectively given me a proper understanding of why these levers have worked for me. Love your work!

There is value in brevity of expression.

My analogy is that LLM inference is like lightning across the sky. You might think the most logical path is a straight line from cloud to ground. In reality the path taken by lightning is in effect a series of micro-decisions. Initial conditions can make the path vary quite a lot.

I do not pretend to be well-acquainted with current work, but might have a clue.

My working theory is that any singular path taken by an LLM has probable error.
Vary the initial conditions enough, and a sum of the paths might tend to optimal.

Looking at the performance measures, there seems to be a distinct “knee” near 4-bit quantizations. As my personal goal is to make good use a modest GPU (repurposed old datacenter GPU), this looks promising.

Was reading one of Roger Schank’s books, where he talked about how “I Ching” tended to “match” in the human mind. Wondered if this might also apply to LLMs. Asked an LLM to re-phrase I Ching in development terms, and incorporated into my project-skeleton. Asked a frontier-LLM if this notion had any value, and it pointed at a recent research paper.

I strongly suspect there is work within the LLM community along this line. :slight_smile:

My theory is that for practical use in development, for well-divided tasks, we might do well with smaller models that use less resource and complete in less time.

Please feel free to tell me that I am wildly wrong in my analogies.
I might even believe you. :slight_smile:

2 Likes

Should add that the fact that 4-bit quantizations work somewhat is not surprising, as that aligns with an old guess. Wrote twenty-odd years ago about an estimate made twenty-odd years before (based on the knowledge of that time).

The weighted inputs to a neuron looked somewhat like a small number (perhaps 4 or 5 bits). That human thought might have a similar granularity seems possible. That an AI trained on human thought might be similar in granularity seems possible.

(Yeh. Nothing remotely precise in the above.)

Want to go even further afield?

Imagine making contact with an alien race that had neurons with 2x or 4x higher fan-in. Imagine that granularity was reflected in their speech and thought. We might find their language impossible to follow without an AI to transcode down to our 4-bit processor. :slight_smile:

1 Like