The 8 GB Lab Book · Chapter 14

Quantization Orin can execute

Board
Jetson Orin Nano Super
Mode
15W
Summary
Q4_K_M decoded 1.18x faster than Q8_0, not 2x, and scored the same 8/10 on the tool cases.

What I expected

I wrote '~2x of Q8' in the predicted column before the run. That was Prior 1: half the bytes give twice the tokens. Chapter 13 showed three runtimes at one effective . I expected tok/s to be bandwidth divided by bytes and nothing else.

Prior 2 was that the two files give the same effective GB/s, because the bus and the engine are the same.

Prior 3 was that Q4 is slightly worse on the 10 regression cases. I expected it to become worse first on the 2 restraint cases.

The theory

Bits per weight from the file, then a bus floor per token

Bits per weight (arithmetic)

4.02 B params in the checkpoint
Q8_0: 4.27 GB x 8 / 4.02 B = 8.50 bits
Q4_K_M: 2.49 GB x 8 / 4.02 B = 4.96 bits
a plain 4.5-bit file would be 2.26 GB
= the M keeps some tensors at 6 bits: +0.23 GB

BUS FLOOR PER TOKEN, 68 GB/s ROOF

Q8: 4.27 GB / 68 GB/s = 63 ms
Q4: 2.49 / 68 = 37 ms
measured: 1/9.6 = 104 ms, 1/11.3 = 89 ms
the rest: Q8 41 ms, Q4 52 ms
= the not-bus part GREW by 11 ms

'the rest' is all that is not weight bytes. It is attention, norms and launches. For Q4, it is also the unpacking of 4.96-bit blocks into numbers that the cores can multiply. That unpacking is compute. At 612 MHz, compute has a cost.

Q8 measured
104 ms
Q8 bus floor
63 ms (arith.)
Q4 measured
89 ms
Q4 bus floor
37 ms (arith.)
Ms per token. The white space right of each floor is what the bytes do not explain. My 2x assumed that space is zero.

Prefill: which silicon is running it (arithmetic from measured tok/s)

Q8 prefill
3.75 TF/s
Q4 prefill
3.34 TF/s
fp32 core peak
1.25 TF/s
2 x 4.02 GFLOP per token times 467 and 416 tok/s. Three times above the CUDA-core peak (1024 x 2 x 0.612 GHz), so prefill must be on the 32 tensor cores; the measurement says which silicon without a profiler. Q4 prefill is 11% slower: on the compute-bound side, unpacking costs and the smaller bytes buy nothing.

The setup

Run (board, ~20 min)
bash tools/day14_quant.sh
cat ~/day14/ram.txt; ls ~/slm/
From q4.json, the file the run wrote
"tag": "q4"
"model": "~/models/Qwen3-4B-Q4_K_M.gguf"
"ctx": 4096
"start_ms": 31228.43
"grammar": false
"chat": [ ... ]
"tool": [ ... ]
"cases": [ ... ]
"summary": { "ttft_ms_chat_median": 159.921, "decode_tps_chat_median": 10.697, "tool_valid": ...
  • Script: tools/day14_quant.sh, which calls tools/slm_eval.py
  • Wrote: day14/bench_Q8_0.json, bench_Q4_K_M.json, ram.txt
  • And: slm/q8.json, slm/q4.json (quality)
  • llama-bench gives prefill and decode tok/s, tegrastats gives the RAM pile with each server up, and slm_eval.py runs the same 10 prompts and 10 tool cases on both files.
Environment
# from day14/bench_Q4_K_M.json and bench_Q8_0.json (this run)
"build_commit": "3466812", "backends": "CUDA", "gpu_info": "Orin"
"n_gpu_layers": 99, "n_threads": 6, "type_k": "f16", "type_v": "f16"
llama-bench -ngl 99 -p 512 -n 128 -r 5 (tools/day14_quant.sh)
# board state captured 2026-09-29 (days/environment/board-2026-09-29.txt), not the state of this day's run
L4T R36.4.2, kernel 5.15.148-tegra
NV Power Mode: 15W
CUDA 12.6 (V12.6.68), TensorRT 10.3.0.30, cuDNN 9.3.0.75

The measurement

WhatExpectedMeasuredNote
Q8_0 size / decode / prefill2x bytes of Q44.27 GB / 9.6 / 467 tok/spile 5259 MiB at 4k, idle 1519
Q4_K_M size / decode / prefill~2x tok/s of Q82.49 GB / 11.3 / 416 tok/spile 4171 MiB at 4k, idle 1519
B_eff Q8 / Q4same41.1 / 28.2 GB/ssame bus
tool call emitted 5 / regression 10 / restraint 2, Q8-5 / 8 / 2slm_eval.py
tool call emitted 5 / regression 10 / restraint 2, Q4slightly worse5 / 8 / 2slm_eval.py
Native precisions on this board-FP16, INT8, INT4sm_87; no NVFP4/FP8/MXFP4
Measured on the board unless the row says estimate or predicted.
Qwen3-4B, Q8_0 vs Q4_K_M: bytes, decode speed, and the tool regression score, 15W
Fig 14.1Qwen3-4B, Q8_0 vs Q4_K_M: bytes, decode speed, and the tool regression score, 15W.

What sm_87 executes; cross out the rest

runsFP16
runsINT8
runsINT4
HopperFP8not on sm_87
BlackwellNVFP4not on sm_87
BlackwellMXFP4not on sm_87
The tensor-core dtypes of this board (sm_87, spec). Q8_0 and Q4_K_M are storage formats, not compute formats: both get unpacked before the multiply. The quant word and the compute word are two different words.
quanttools /5cases /10restraint /2chat TTFT ms
Q8_0582162
Q4_K_M582160
Quality, same harness (measured, slm_eval.py). Identical, column for column. The arithmetic below gives what 10 cases can see.

What 10 cases can see (arithmetic)

say Q4 truly fails 1 in 20 more (p = 0.05)
10 cases show nothing: 0.95^10 = 0.60
2 restraint cases show nothing: 0.95^2 = 0.90
to catch p at 95%: n = ln 0.05 / ln 0.95 = 59 cases
with n = 10, the smallest p I catch at 95%: 26%
= this set is blind below ~26% per case

On this set, the two files scored 8/10 and failed the same two cases. The set detects a loss for each case only at approximately 26% or more.

Where it broke

For Prior 1, I expected 2x and measured 1.18x. My model had no constant term. It is also not one shared constant. The line through the two measured points needs 1.78 GB / 15.5 ms = 115 GB/s of weight streaming. That value is above the roof. The Q4 kernel has its own extra work.

For Prior 2, the result was 41.1 against 28.2 GB/s effective. The bus is the same, but the kernel that uses it is different. For Prior 3, the scores are identical, 5/5, 8/10, 2/2, and the same two cases failed.

I predicted 2x from a model with no constant term. I did this one day after Chapter 13 gave me a 25 GB/s limit. That limit IS a constant term in a different form. I trusted this measure yesterday, and I forgot it today.

The pile is the whole board, so I subtract the idle figure from the same run (ram.txt: idle 1519). Q8: 5259 - 1519 = 3740 . Q4: 4171 - 1519 = 2652 MiB. counts MiB, so the files are 4.27 GB = 4076 MiB and 2.49 GB = 2376 MiB. KV at -c 4096 is 4096 x 144 KiB = 576 MiB (604 MB), the Chapter 3 figure, allocated at start in both. So the Q8 pile sits 336 MiB under its own file, and the Q4 pile 276 MiB over it, before KV and context. The used-RAM counter does not show all the 'd weight pages. Most likely they count as file cache, which tegrastats excludes. Not tested.

Summary

  • is not 4 bits. 2.49 GB for 4.02B weights is approximately 5 bits for each weight. K-quants keep wider blocks where the model is sensitive. The file size is the only reliable source.
  • Q4 and Q8 had the same scores, 5/5, 8/10 and 2/2, and failed the same two cases. Ten cases detect a loss for each case only at approximately 26% or more. The restraint cases did not become worse first.
  • sm_87 has tensor-core paths for , and INT4 (spec). It has no path for NVFP4, FP8 or MXFP4. Those are Hopper and Blackwell formats.

Sources

These sources are not mine. Measured numbers came off my own board; specs, estimates and paper figures are labeled as such.

  • TensorRT Edge-LLM supported-models / precision notes (Orin: FP16, INT8, INT4 only)
  • GGUF quant table (Q4_K_M as the usual default)
  • AWQ paper for the idea of protecting salient weights