Using fewer bits for a matrix can reduce its stored numeric payload while changing the model computation. This activity packs actual trained-course weights, dequantizes them for the existing CPU predictor, and measures both payload and output differences. It keeps the original checkpoint intact.
When you need this. You want to inspect where a smaller weight representation saves bytes and where its rounding changes predictions. Measure the packed bytes and scales directly; a file labelled INT4 would not prove its values were packed or that inference uses integer arithmetic.
A row scale maps a matrix row to a symmetric integer range. The code divides by the scale, rounds and clamps, then stores the codes. Dequantization multiplies each restored code by the row scale. It quantizes the selected linear matrices only; embeddings, normalization parameters and biases remain float64. The implementation is a small explicit format, not an activation-aware quantization method or a specialized inference kernel.
The lab loads the byte-preserved trained story-model params.json and its tokenizer. For each selected matrix it computes scales, writes actual byte payloads, and reconstructs a float64 tensor. The existing decoder consumes those reconstructed tensors. A full-precision forward pass on the same input supplies the logit and argmax reference.
The copied trained parameters and tokenizer come from the existing story-model training lesson. Their original files remain unchanged.
Download the complete lab and enter quantization-and-batching/. Create an isolated Python environment and install its pinned dependency with python -m pip install -r requirements.txt. The computation uses CPU tensors; no pretrained model download, GPU or paid service is needed. The PyTorch installation guide covers platform-specific installation choices. Another library version requires rerunning the checks.
1. Inspect the row scale and packed representation
For a row with maximum absolute value m, the scale is m/qmax; an all-zero row uses scale 1. qmax is 127 for eight bits or 7 for four bits. The four-bit format uses integer values -7 through 7, offsets each by 8, and stores adjacent codes in low and high nibbles of one byte. An odd final code gets a zero-valued padding code that is removed using the saved shape.
test_signed_four_bit_packing_and_odd_lengths checks signed values and an odd-length row. test_zero_row_and_error_bound_for_both_widths checks zero-row handling and reconstruction within half a row scale. These inspect the stored representation, not just a tensor cast.
2. Compare payload and model output
python run.py quantizationThe input is mia has a red hat., encoded as [58,36,33,89,79,4]. The selected matrices are Wq, Wk, Wv, Wo, W1, W2, W_out. Full-precision numeric parameter payload is 59520 bytes. The following table reads the actual result:
| Width | Numeric model payload | Selected packed matrices and scales | Maximum logit error | Changed argmax positions |
|---|---|---|---|---|
8 bits |
28544 bytes |
5888 bytes |
0.094337 |
0/6 |
4 bits |
26240 bytes |
3584 bytes |
1.383815 |
1/6 |
The numeric payload includes float64 row scales and untouched parameters. It excludes container metadata; the raw record separately reports JSON serialization sizes. It is not measured process memory: this experiment retains full-precision and reconstructed tensors while comparing them. Unchanged argmax positions can still have different logits.
Read the complete implementation
"""Actual CPU weight quantization and token-step scheduling experiments.
The scheduler uses logical arrival ticks, not measured network latency.
"""
import copy
import json
import math
from pathlib import Path
import torch
import torch.nn.functional as F
import decoder
from bpe import Tokenizer
HERE=Path(__file__).resolve().parent
MATRICES=['Wq','Wk','Wv','Wo','W1','W2','W_out']
def parameters():
raw=json.loads((HERE/'params.json').read_text())
decoder.CONFIG['d']=len(raw['tok_emb'][0])
return {name:torch.tensor(value,dtype=torch.float64) for name,value in raw.items()}
def quantize(weight,bits):
if bits not in (4,8) or weight.ndim!=2 or min(weight.shape)<1 or not torch.isfinite(weight).all():
raise ValueError('finite matrix and 4 or 8 bits required')
qmax=2**(bits-1)-1
maximum=weight.abs().amax(1)
scales=torch.where(maximum==0,torch.ones_like(maximum),maximum/qmax)
q=(weight/scales[:,None]).round().clamp(-qmax,qmax).to(torch.int8)
if bits==8:
payload=bytes(q.view(torch.uint8).flatten().tolist())
else:
shifted=(q.flatten().to(torch.int16)+8).to(torch.uint8)
if len(shifted)%2:shifted=torch.cat((shifted,torch.tensor([8],dtype=torch.uint8)))
payload=bytes((shifted[::2] | (shifted[1::2]<<4)).tolist())
return {'bits':bits,'shape':list(weight.shape),'scales':scales.tolist(),'payload_hex':payload.hex()}
def dequantize(artifact):
bits=artifact['bits'];rows,cols=artifact['shape']
if bits not in (4,8) or type(rows) is not int or type(cols) is not int or rows<1 or cols<1:
raise ValueError('invalid quantized shape or width')
scales=torch.tensor(artifact['scales'],dtype=torch.float64)
if scales.shape!=(rows,) or not torch.isfinite(scales).all() or (scales<=0).any():
raise ValueError('invalid row scales')
data=bytearray.fromhex(artifact['payload_hex'])
required=rows*cols if bits==8 else (rows*cols+1)//2
if len(data)!=required:raise ValueError('quantized payload size mismatch')
packed=torch.tensor(list(data),dtype=torch.uint8)
if bits==8:q=packed.view(torch.int8).to(torch.float64)
else:q=torch.stack((packed&15,packed>>4),1).flatten()[:rows*cols].to(torch.float64)-8
return q.reshape(rows,cols)*scales[:,None]
def batched_logits(p,sequences):
if not sequences:raise ValueError('nonempty batch required')
for ids in sequences:decoder.inputs(p,ids,[0]*len(ids))
length=max(map(len,sequences));d=p['tok_emb'].shape[1]
ids=torch.zeros((len(sequences),length),dtype=torch.long)
sizes=torch.tensor([len(s) for s in sequences])
for i,sequence in enumerate(sequences):ids[i,:len(sequence)]=torch.tensor(sequence)
x=p['tok_emb'][ids]+p['pos_emb'][:length]
norm=lambda value,name:F.layer_norm(value,(d,),p[name+'_g'],p[name+'_b'],eps=decoder.model.EPS)
h=norm(x,'ln1');q,k,v=(h@p[name] for name in ['Wq','Wk','Wv'])
scores=(q@k.transpose(-1,-2))/math.sqrt(d)
causal=torch.ones(length,length,dtype=torch.bool).tril()
valid=torch.arange(length)[None,:]<sizes[:,None]
allowed=causal[None,:,:]&valid[:,None,:]
mixed=scores.masked_fill(~allowed,float('-inf')).softmax(-1)@v
x=x+mixed@p['Wo'];h=norm(x,'ln2')
x=x+F.relu(h@p['W1']+p['b1'])@p['W2']+p['b2']
logits=norm(x,'lnf')@p['W_out']+p['b_out']
return torch.stack([logits[i,n-1] for i,n in enumerate(sizes)])
def schedule(p,jobs,continuous=True,capacity=2):
if type(capacity) is not int or capacity<1:raise ValueError('positive capacity required')
if not isinstance(jobs,list) or not jobs or len(jobs)>64:
raise ValueError('1..64 jobs required')
if any(not isinstance(job,dict) or set(job)!={'id','arrival','prompt','tokens'} or not isinstance(job['id'],str) or not job['id'] for job in jobs):
raise ValueError('named jobs with arrival, prompt and tokens required')
if len({job['id'] for job in jobs})!=len(jobs):raise ValueError('unique job IDs required')
for job in jobs:
if type(job['arrival']) is not int or job['arrival']<0 or type(job['tokens']) is not int or job['tokens']<1:
raise ValueError('nonnegative arrival and positive token budget required')
decoder.inputs(p,job['prompt'],[0]*len(job['prompt']))
if len(job['prompt'])+job['tokens']>len(p['pos_emb']):raise ValueError('context budget exceeded')
pending=sorted(copy.deepcopy(jobs),key=lambda j:(j['arrival'],j['id']));active=[];done=[];ticks=[];tick=0
with torch.no_grad():
while pending or active:
if continuous or not active:
while pending and pending[0]['arrival']<=tick and len(active)<capacity:
job=pending.pop(0);job['generated']=[];job['admitted']=tick;active.append(job)
if not active:
tick=pending[0]['arrival'];continue
logits=batched_logits(p,[j['prompt']+j['generated'] for j in active])
tokens=logits.argmax(-1).tolist();ticks.append({'tick':tick,'active':[j['id'] for j in active]})
keep=[]
for job,token in zip(active,tokens):
job['generated'].append(token)
if len(job['generated'])==job['tokens']:
job['finished']=tick+1;done.append(job)
else:keep.append(job)
active=keep;tick+=1
return {'policy':'continuous admission' if continuous else 'fixed batch until all finish',
'capacity':capacity,'logical_ticks':tick,'batch_forward_calls':len(ticks),
'jobs':sorted(done,key=lambda j:j['id']),'trace':ticks}
def quantization_run():
p=parameters();tok=Tokenizer.load(HERE/'tokenizer.json');text='mia has a red hat.';ids=tok.encode(text)
baseline,_=decoder.forward(p,ids,[0]*len(ids));rows=[]
full=sum(value.numel()*value.element_size() for value in p.values())
for bits in [8,4]:
changed={name:value.clone() for name,value in p.items()};packed={}
for name in MATRICES:
packed[name]=quantize(p[name],bits);changed[name]=dequantize(packed[name])
logits,_=decoder.forward(changed,ids,[0]*len(ids))
payload=sum(len(bytes.fromhex(a['payload_hex']))+len(a['scales'])*8 for a in packed.values())
untouched=sum(value.numel()*value.element_size() for name,value in p.items() if name not in MATRICES)
rows.append({'bits':bits,'selected_matrices':MATRICES,'numeric_model_payload_bytes':payload+untouched,
'packed_matrix_bytes':payload,'unquantized_parameter_bytes':untouched,
'serialized_quantized_artifacts_bytes':len(json.dumps(packed).encode()),
'max_logit_error':(logits-baseline).abs().max().item(),
'changed_argmax_positions':int((logits.argmax(-1)!=baseline.argmax(-1)).sum()),
'positions':len(ids)})
return {'mode':'actual local CPU quantization, packing and dequantized inference on an existing trained checkpoint',
'input_text':text,'input_ids':ids,'full_precision_payload_bytes':full,'results':rows,
'scope':'Selected linear matrices only; scales remain float64. No integer kernel, AWQ, GPU speed or quality benchmark.'}
def batching_run():
p=parameters();tok=Tokenizer.load(HERE/'tokenizer.json')
jobs=[{'id':'a','arrival':0,'prompt':tok.encode('mia has'),'tokens':2},
{'id':'b','arrival':0,'prompt':tok.encode('zoe has'),'tokens':5},
{'id':'c','arrival':0,'prompt':tok.encode('ben has'),'tokens':1}]
fixed=schedule(p,jobs,False);continuous=schedule(p,jobs,True)
same=[j['generated'] for j in fixed['jobs']]==[j['generated'] for j in continuous['jobs']]
if not same:raise AssertionError('scheduling changed deterministic output')
return {'mode':'actual local CPU batched token decoding under authored logical arrival ticks',
'fixed':fixed,'continuous':continuous,'outputs_equal':same,
'scope':'Logical ticks are not wall-clock latency or throughput. Full-prefix recomputation; no paged KV cache or vLLM claim.'}{
"mode": "actual local CPU quantization, packing and dequantized inference on an existing trained checkpoint",
"input_text": "mia has a red hat.",
"input_ids": [
58,
36,
33,
89,
79,
4
],
"full_precision_payload_bytes": 59520,
"results": [
{
"bits": 8,
"selected_matrices": [
"Wq",
"Wk",
"Wv",
"Wo",
"W1",
"W2",
"W_out"
],
"numeric_model_payload_bytes": 28544,
"packed_matrix_bytes": 5888,
"unquantized_parameter_bytes": 22656,
"serialized_quantized_artifacts_bytes": 13296,
"max_logit_error": 0.09433748097707007,
"changed_argmax_positions": 0,
"positions": 6
},
{
"bits": 4,
"selected_matrices": [
"Wq",
"Wk",
"Wv",
"Wo",
"W1",
"W2",
"W_out"
],
"numeric_model_payload_bytes": 26240,
"packed_matrix_bytes": 3584,
"unquantized_parameter_bytes": 22656,
"serialized_quantized_artifacts_bytes": 8462,
"max_logit_error": 1.3838154876852728,
"changed_argmax_positions": 1,
"positions": 6
}
],
"scope": "Selected linear matrices only; scales remain float64. No integer kernel, AWQ, GPU speed or quality benchmark.",
"refusal": "finite matrix and 4 or 8 bits required",
"command": [
"python",
"run.py",
"quantization",
"--record"
],
"exit_status": 0,
"environment": {
"python": "3.12.8",
"torch": "2.14.1",
"platform": "macOS-27.0.1-arm64-arm-64bit"
},
"sources": {
"experiment.py": "1306ee38f04deea2060cdbf6fa7449da38279f9fe7be2eec8893d3a2afe8a9eb",
"decoder.py": "5706ca21af551b17ee54ca00f5a6011981b327e03b1e880b6712c06dd7d1c39a",
"vendor/model.py": "0d3d469afd5f35ef19b04e0b0587143c39f8fbca37d6154e6435c4ed4e8b80fc",
"run.py": "aaac2de2d29a3e5f70f654839dc454cd8ef0ac97fa14e870d245d28ab65d3022",
"test_experiment.py": "f9c08f86d6ce9f9a5d78dd6acf572f81431711992e709ac6106c17b90f76d04d",
"params.json": "26510c85e6e6bf6e9b1dd8766f20a062ce6da35eff4d50984f1b2aead774c1d5",
"tokenizer.json": "7478ffcb69ca5383828956b1d6db10f2ca6bc925abab7122e8da6f5d05e61657",
"bpe.py": "f2986e70b69224d9bae7694b085275d01d8cf0103b7f05399e8c9840a1679143"
}
}
run.py quantization performs new local computation and leaves saved files unchanged. --record deliberately replaces this stage’s results and report. The raw record retains the command, exit status, runtime and implementation hashes.
Failure cases
| Boundary checked | Required behavior |
|---|---|
| Invalid packed representation | Refuse unsupported widths, empty axes, malformed shapes, payload lengths and scales. |
| Signed packing with an odd count | Restore signed codes exactly in the controlled test and discard the final padding nibble using the stored shape. |
| Zero-row and reconstruction bound | Keep zero rows finite and reconstruction error within half the row scale. |
The representation cannot be decoded. test_invalid_width_payload_and_scales_refused rejects unsupported bit widths, empty matrix axes, mismatched payload sizes, invalid shapes and nonpositive row scales. The saved invalid-width refusal is finite matrix and 4 or 8 bits required. Correct the representation instead of supplying zero weights to make inference continue.
The lower-bit model changes an output. The changed-argmax count in the table is an observed consequence, not a parser failure. Inspect the logits at those input positions before deciding whether a compressed model meets a task requirement. One short input cannot establish generation quality or an acceptable accuracy tradeoff.
Run python -m unittest -v test_experiment.py. In a separate copy, inspect an all-zero row and an odd-length row before dequantization. Explain why the saved shape is needed even when the packed byte count is correct.
Interpret the result at its actual scope
The code performs floating-point inference after dequantization. It does not execute INT8 or INT4 matrix-multiplication kernels, implement AWQ, compare GPU speed, or measure throughput. Keep weight-storage comparisons separate from deployment memory, latency and task-quality measurements. The original trained parameters and earlier recordings remain unchanged.
Check your understanding
Read the explanation
Actual codes are packed, then reconstructed for the existing predictor. Those are the measured boundaries.
References
Primary sources checked October 24, 2026. The saved local execution, rather than the cited research results, supplies the measurements above.