Research
LYR Performance Note #030 Models

Generation speed is decided by the byte count of the weights, not the parameter count

4B-INT8 and 8B-INT4 came out at the same speed — and get one thing wrong in how you measure, and 2.35x looks like "no difference"

2026-07-23 Part 11 8 min quantizationinferencememory bandwidthmeasurementGPU

Measured on the same GPU, 4B-INT8 (weights about 4.0GB) and 8B-INT4 (about 4.3GB) came out at nearly the same speed. 5.98ms against 6.50ms per token. Even though the parameter counts differ by 2x.

The reason is simple: every time it generates one token, it reads out the model’s weights in full. What decides it isn’t the amount of compute and isn’t the parameter count — it’s the number of bytes it has to read. Which is also why, when you pick a GPU, you look at memory bandwidth and not compute performance.

This law comes with premises, though. In fact, my first measurement got the method wrong and I misread a “should be 2x faster” as 1.3x — and under a different measurement method, a 2.35x gap disappeared, and then inverted.

4B (4 billion params) 8B (8 billion) Bar length = generation time per token 4B-INT4 2.6GB 4.04ms 4B-INT8 4.0GB 5.98ms 8B-INT4 4.3GB 6.50ms Close in bytes = nearly same speed 4B-FP16 7.6GB 9.49ms Parameter count (color) jumps around — 4B, 4B, 8B, 4B — but generation time is monotonic in byte count. * Same GPU, batch=1, same quantization kernel, LoRA merged.
Figure 1: the byte count of the weights against generation time per token. Color is the parameter count (green = 4B, red = 8B). The ordering swaps around — 4B, 4B, **8B**, 4B — and yet generation time is cleanly monotonic in byte count. 4B-INT8 (4.0GB) and 8B-INT4 (4.3GB) in particular land at nearly the same speed, despite a 2x difference in parameters.
Note: a comparison held constant on the same GPU, batch=1, the same quantization kernel, and with LoRA merged.

”A 4B should be 2x faster” only came out at 1.3x

The first measurement went like this. Swap the 8B model for a 4B and you halve the weights you read, so it should be close to 2x faster. But what I measured stopped at 1.24–1.36x.

“Going to 4B doesn’t buy you as much as you’d think” — if I had drawn my conclusion right there, I would have gotten the whole model-size decision wrong.

The cause wasn’t the model. It was that the setups I was measuring weren’t held constant. The 4B side alone was carrying a double handicap.

  • A different quantization kernel (one of them was a scheme that unpacks less efficiently on that GPU generation)
  • The adapter was still running bolted on the outside (every generation step pays an extra small matrix multiply)

Re-measure with the same quantization scheme as the 8B side, and with the adapter merged into the model itself — 2.3x. It had followed the law all along.

Hold the serving setup constant before you compare. A comparison that isn’t held constant makes 2x look like 1.3x.

Every token reads all of the weights

Why does the byte count decide it? The mechanism is plain.

When a language model generates one token, it touches every weight in the model exactly once. So at batch=1 (generating for a single user only), the read out of memory dominates rather than the compute. The amount read is the byte count of the weights, so generation speed is inversely proportional to that byte count.

Every token you make reads the weights end to end, once VRAM (memory) where the model's weights sit per-layer weights (a few GB in all) Every token: read it all This pipe = memory bandwidth Compute units wait until the read ends The math itself takes less time than the wait So one token's time is set by the bytes it reads INT4 2.6GB 4.04ms FP16 7.6GB 9.49ms ■ reading the weights ■ everything else
Figure 2: why the byte count decides it. Every token you make reads the weights sitting in VRAM from beginning to end, once. The compute units spend more time waiting for that read to finish than working, and what's rate-limiting is not compute but memory bandwidth. So 2.6GB of weights (INT4) is 4.04ms and 7.6GB (FP16) is 9.49ms — the time stretches in proportion to how much you read.
Note: this is the batch=1 case (generating for a single user). Process several users together and one read can be shared, which changes the story.

The same law carries straight over to quantization bit width. Bit width is byte count.

PrecisionWeight sizePer tokenvs FP16
FP167.6GB9.49ms1.00x
INT84.3GB6.00ms1.58x
INT42.6GB4.04ms2.35x

The size ratio is 2.92x but the measured value is 2.35x. The gap is what’s left over — the parts that aren’t quantized (reads and writes of the cache and the activations). Byte count decides everything, but there are reads and writes beyond the model’s weights.

And as long as memory bandwidth is the rate limiter, the number to look at when choosing a GPU is bandwidth too. Not compute performance, and not VRAM capacity.

Get one thing wrong in how you measure and the 2.35x disappears

Now for the real point. Same model, same GPU, measured three times changing nothing but the measurement method. The results split three ways.

How it was measuredFP16INT4What it looks like
Stock inference path40ms39msLevel (no difference)
Inference server (eager execution)20.9ms22.2msInverted (quantizing makes it slower)
Inference server (graph execution)9.49ms4.04ms2.35x (correct)

In the first, the fast quantization kernel never got used and compute was the rate limiter. In the second, the fixed per-step overhead dominated, and on top of that only the unpacking cost of quantization got added. Only in the third did the fixed cost come out and the bandwidth-bound shape appear.

The speed of quantization only shows up when “fast kernel × graph execution × batch=1 bandwidth-bound” all line up. Drop any one of the three and it doesn’t just look like it “doesn’t work” — it looks actively harmful. When you’re reading benchmark numbers, check first that all three line up.

Open questions: a law has a domain where it holds

This law isn’t unconditional. I’ve confirmed two conditions, by measurement, where it breaks.

The first is when you’re handling several users at once. Process the generation in a batch and several requests can share a single read of the weights. Throughput scales with bandwidth, but the wait for any one user stays proportional to the byte count. Everything in this article is about the speed of generating for a single user.

The second is when the weights get too small. Quantize a small model hard and put it on a fast GPU, and the weight read finishes so quickly that fixed costs like kernel launch become the dominant term. In practice, 4B-INT4 came out at 168 against 177 tok/s across the top two generations of data-center GPU — barely any difference at all, even with 3.3x the bandwidth. Measure the same 8B-INT4 on a previous-generation consumer GPU and it ran at exactly half the speed of the 4B (= plainly bandwidth-bound).

So don’t try to confirm this law with a comparison that spans GPUs. If one side is bandwidth-bound and the other is fixed-cost-bound, the same law will hold in one place and fail in the other.

Lessons

  1. Generation speed is decided by the byte count of the weights, not the parameter count. 4B-INT8 and 8B-INT4 came out at nearly the same speed. So design around “light in bytes” rather than “a small model,” and choose GPUs on memory bandwidth rather than compute performance.
  2. Hold the serving setup constant before you compare. A difference in nothing but the quantization kernel and how the adapter was handled turned 2x into 1.3x. Before you attribute a gap to model size, hold the way it’s run constant.
  3. A law has a domain where it holds. Step outside the batch=1, bandwidth-bound premise and neither size nor bandwidth buys you anything. In comparisons that span GPUs, suspect that the premise itself has changed.

This is the manifesto’s first principle — Measure First — in its inference-performance form. Same model, same GPU, and still, one choice in how you measure makes 2.35x look like “no difference,” or like an inversion.


Appendix: raw data

All measured at batch=1 (generating for a single user). TPOT = generation time per token.

ItemMeasuredConditions & caveats
Monotonicity of bytes and speed4B-INT4 2.6GB / 4.04ms < 4B-INT8 4.0GB / 5.98ms ≈ 8B-INT4 4.3GB / 6.50ms < 4B-FP16 7.6GB / 9.49msSame GPU, same quantization kernel, adapter merged. Monotonic in byte-count order, not parameter-count order
Effect of halving the parameters8B-INT4 76 tok/s → 4B-INT4 172 tok/s = 2.3xWith the same quantization and merged adapters held constant. Consistent with 4B-INT4 at 166 tok/s on a different GPU
Mis-measurement (setup mismatch)4B served with a bolted-on adapter + a different quantization scheme → 91 tok/s = 1.24–1.36x over the 8BA double handicap: the efficiency gap between quantization kernels plus adapter overhead. The 8B side was partly under the same conditions, so it’s asymmetric
Effect of bit widthFP16 105 / INT8 167 / INT4 247 tok/sAgainst a size ratio of 2.92x, the measured value is 2.35x. The gap is the reads and writes that aren’t quantized (cache, activations, embeddings) plus fixed kernel cost
The measurement trapstock path 40 / 39ms (level) → eager execution 20.9 / 22.2ms (inverted) → graph execution 9.49 / 4.04ms (correct)The 2.35x only comes out once fast kernel, graph execution and batch=1 bandwidth-bound all line up
Outside the domain (fixed-cost-bound)4B-INT4 came out 168 against 177 tok/s across the top two generations of data-center GPU (no response to 3.3x the bandwidth)The weights are too small, so fixed costs like kernel launch dominate. 8B-INT4 is plainly bandwidth-bound on a previous-generation consumer GPU
Side benefitOnce the quantization schemes were held constant, a bug where characters from another language crept in also went awayThe quantization scheme can affect not just speed but the language consistency of the output