When you need this. A service owner wants lower memory use without silently degrading code-assistant responses. Apply a real quantizer, preserve the baseline, and evaluate a candidate before treating lower precision as an optimization.
What you run. Fresh CPU INT8 model execution and recorded development-text likelihoods. No FP8, INT4, or NVFP4 device execution is claimed.
This lesson builds on reuse prompt prefixes without overloading a warm replica.
Evaluate the converted model
Quantization represents values with fewer bits and associated scales. Weight precision, activation precision, and KV precision are separate decisions. Supported kernels and conversion methods depend on the runtime and hardware.
The live experiment applies dynamic INT8 quantization to the model’s linear modules on CPU. It compares the likelihood of manually written development texts before and after conversion. Perplexity here summarizes next-token negative log likelihood; lower is better on these texts. It is not an assessment of code correctness or broad model quality.
quantize_live.py loads the pinned model, scores the same development texts, converts real linear modules, and records the new scores. It uses a pinned legacy PyTorch quantization API whose deprecation warning is preserved in the run notes; it is an investigation of a candidate, not a recommended new deployment API.
Inspect the quantized candidate
Download the lab from this page, extract it, and run these commands from its directory. The default command reads saved run records or runs the explicitly described local algorithm; it never downloads a model or calls a paid API.
python3 lab.py
python3 -m unittest -v test_lab.pyRead lab.py
"""Compare actual baseline and quantized-model development likelihood records offline."""
import json, math
from pathlib import Path
def compare_scores(baseline, candidate):
if not baseline or len(baseline) != len(candidate):
raise ValueError("Matched development cases are required")
for a, b in zip(baseline, candidate):
if a["text"] != b["text"] or a["predicted_tokens"] != b["predicted_tokens"]:
raise ValueError("Cases or token denominators differ")
if a["predicted_tokens"] < 1 or any(
not math.isfinite(x["nll"]) or x["nll"] < 0 for x in [a, b]
):
raise ValueError("Invalid likelihood data")
tokens = sum(a["predicted_tokens"] for a in baseline)
before = math.fsum(a["nll"] * a["predicted_tokens"] for a in baseline) / tokens
after = math.fsum(a["nll"] * a["predicted_tokens"] for a in candidate) / tokens
return {
"predicted_tokens": tokens,
"baseline_token_weighted_nll": before,
"candidate_token_weighted_nll": after,
"nll_increase": after - before,
"likelihood_regression": after > before,
}
def main():
record = json.loads(Path("quantization-run.json").read_text())
print(
json.dumps(
{
"execution_mode": "offline analysis of fresh CPU INT8 model execution",
"model_id": record["model_id"],
"model_revision": record["model_revision"],
"quantized_linear_modules": record["quantized_linear_modules"],
"comparison": compare_scores(
record["baseline_fp32"]["cases"], record["candidate_int8"]["cases"]
),
"limits": "Development likelihood only; no held-out task-quality, GPU performance or production acceptance.",
},
indent=2,
)
)
if __name__ == "__main__":
main()Read quantize_live.py
"""Real CPU INT8 dynamic quantization and development-text likelihood comparison."""
import argparse
import copy
import hashlib
import json
import math
import platform
import time
from pathlib import Path
def score(model, tokenizer, texts, torch):
results = []
with torch.inference_mode():
for text in texts:
ids = tokenizer(text, return_tensors="pt")["input_ids"]
start = time.perf_counter()
logits = model(ids, use_cache=False).logits
loss = torch.nn.functional.cross_entropy(
logits[:, :-1].float().reshape(-1, logits.shape[-1]),
ids[:, 1:].reshape(-1),
)
results.append(
{
"text": text,
"predicted_tokens": ids.shape[-1] - 1,
"nll": float(loss),
"elapsed_ms": round((time.perf_counter() - start) * 1000, 3),
}
)
weighted = sum(r["nll"] * r["predicted_tokens"] for r in results) / sum(
r["predicted_tokens"] for r in results
)
return {
"cases": results,
"token_weighted_nll": weighted,
"perplexity": math.exp(weighted),
}
def main():
p = argparse.ArgumentParser()
p.add_argument("--model-path", required=True)
p.add_argument("--output", required=True)
a = p.parse_args()
import torch, transformers
from transformers import AutoModelForCausalLM, AutoTokenizer
torch.set_num_threads(4)
torch.manual_seed(0)
tokenizer = AutoTokenizer.from_pretrained(a.model_path, local_files_only=True)
model = AutoModelForCausalLM.from_pretrained(
a.model_path,
local_files_only=True,
dtype=torch.float32,
attn_implementation="eager",
).eval()
texts = [
"An inference service should release the request cache when a client cancels.",
"A bounded queue rejects excess requests instead of allowing memory to grow without a limit.",
"Measure time to first token separately from the time between later output tokens.",
]
baseline = score(model, tokenizer, texts, torch)
torch.backends.quantized.engine = "qnnpack"
quantized = torch.ao.quantization.quantize_dynamic(
model, {torch.nn.Linear}, dtype=torch.qint8, inplace=False
).eval()
candidate = score(quantized, tokenizer, texts, torch)
qmodules = sum(
isinstance(m, torch.ao.nn.quantized.dynamic.Linear) for m in quantized.modules()
)
report = {
"execution_mode": "fresh local CPU inference; dynamic INT8 Linear modules",
"environment": {
"python": platform.python_version(),
"torch": torch.__version__,
"transformers": transformers.__version__,
"platform": platform.platform(),
"quantized_engine": torch.backends.quantized.engine,
"threads": torch.get_num_threads(),
},
"model_id": "Qwen/Qwen2.5-0.5B-Instruct",
"model_revision": "7ae557604adf67be50417f59c2c2f167def9a775",
"license": "Apache-2.0",
"source_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
"quantized_linear_modules": qmodules,
"baseline_fp32": baseline,
"candidate_int8": candidate,
"perplexity_delta": candidate["perplexity"] - baseline["perplexity"],
"limits": "Three manually written development texts, not held-out task-quality evaluation. Timing includes forward and loss calculation. No GPU quantized kernels, FP8, INT4, or NVFP4 tested.",
}
Path(a.output).write_text(json.dumps(report, indent=2) + "\n")
if __name__ == "__main__":
main()The real converted model worsened the development-text likelihood score. That result rejects this particular candidate rather than proving that INT8 is generally unsuitable. A different supported quantizer and a proper task evaluation may lead to a different decision.
Inspect the complete local result
{
"execution_mode": "offline analysis of fresh CPU INT8 model execution",
"model_id": "Qwen/Qwen2.5-0.5B-Instruct",
"model_revision": "7ae557604adf67be50417f59c2c2f167def9a775",
"quantized_linear_modules": 169,
"comparison": {
"predicted_tokens": 41,
"baseline_token_weighted_nll": 6.035825868932212,
"candidate_token_weighted_nll": 7.285529497193127,
"nll_increase": 1.2497036282609146,
"likelihood_regression": true
},
"limits": "Development likelihood only; no held-out task-quality, GPU performance or production acceptance."
}Inspect the actual execution record
{
"execution_mode": "fresh local CPU inference; dynamic INT8 Linear modules",
"environment": {
"python": "3.12.8",
"torch": "2.14.1",
"transformers": "5.20.2",
"platform": "macOS-27.0.1-arm64-arm-64bit",
"quantized_engine": "qnnpack",
"threads": 4
},
"model_id": "Qwen/Qwen2.5-0.5B-Instruct",
"model_revision": "7ae557604adf67be50417f59c2c2f167def9a775",
"license": "Apache-2.0",
"source_sha256": "c4d751f71c0c4a409fe091000f640597b21f8a41cd8ef310f783f6ed4ade98ab",
"quantized_linear_modules": 169,
"baseline_fp32": {
"cases": [
{
"text": "An inference service should release the request cache when a client cancels.",
"predicted_tokens": 13,
"nll": 6.5931525230407715,
"elapsed_ms": 114.118
},
{
"text": "A bounded queue rejects excess requests instead of allowing memory to grow without a limit.",
"predicted_tokens": 15,
"nll": 5.608316421508789,
"elapsed_ms": 87.19
},
{
"text": "Measure time to first token separately from the time between later output tokens.",
"predicted_tokens": 13,
"nll": 5.9717793464660645,
"elapsed_ms": 70.992
}
],
"token_weighted_nll": 6.035825868932212,
"perplexity": 418.14399921617934
},
"candidate_int8": {
"cases": [
{
"text": "An inference service should release the request cache when a client cancels.",
"predicted_tokens": 13,
"nll": 7.135319709777832,
"elapsed_ms": 554.379
},
{
"text": "A bounded queue rejects excess requests instead of allowing memory to grow without a limit.",
"predicted_tokens": 15,
"nll": 7.456194877624512,
"elapsed_ms": 71.903
},
{
"text": "Measure time to first token separately from the time between later output tokens.",
"predicted_tokens": 13,
"nll": 7.2388176918029785,
"elapsed_ms": 61.35
}
],
"token_weighted_nll": 7.285529497193127,
"perplexity": 1459.033482494216
},
"perplexity_delta": 1040.8894832780368,
"limits": "Three manually written development texts, not held-out task-quality evaluation. Timing includes forward and loss calculation. No GPU quantized kernels, FP8, INT4, or NVFP4 tested."
}
This image is an application-generated report of the saved program result, not a new inference run. Open the full-resolution report or read the raw result.
Reject an unacceptable candidate
A smaller representation is useful only if the runtime executes it efficiently and the application accepts its output quality. This experiment produced a regression; the candidate is not accepted. CPU quantization results do not establish FP8 or INT4 behavior on a GPU. FP8, INT4, and NVFP4 need separate supported implementations and evaluations; none was tested here.
Mismatched evaluation cases. The analyzer refuses to compare different texts or token denominators, because such a change would confound the quantization comparison.
Invalid likelihood data. A negative or nonfinite likelihood value is rejected. A valid higher candidate likelihood loss remains visible as a regression rather than being relabeled a pass.
Read the executable checks
import unittest
from lab import compare_scores
class QuantizationEvidenceTests(unittest.TestCase):
def case(self, nll=2, text="text"):
return {"text": text, "predicted_tokens": 3, "nll": nll}
def test_likelihood_regression_is_visible(self):
report = compare_scores([self.case()], [self.case(3)])
self.assertTrue(report["likelihood_regression"])
self.assertEqual(report["nll_increase"], 1)
def test_comparing_different_cases_is_rejected(self):
with self.assertRaises(ValueError):
compare_scores([self.case()], [self.case(text="other")])
def test_invalid_likelihood_is_rejected(self):
for nll in [float("nan"), float("inf"), -1]:
with self.assertRaises(ValueError):
compare_scores([self.case()], [self.case(nll)])
if __name__ == "__main__":
unittest.main()
class GuardBoundaryTests(unittest.TestCase):
def test_rejects_invalid_boundary_inputs_with_specific_reason(self):
import lab
cases = [('compare_scores([],[])', 'Matched development')]
for code, reason in cases:
with self.subTest(code=code):
with self.assertRaisesRegex((ValueError, MemoryError, KeyError, RuntimeError), reason):
exec(code, vars(lab).copy())Use the pinned live runtime from the first lesson for model commands. A path placeholder refers to your explicitly downloaded model; these commands do not fetch one automatically.
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements-live.txt
.venv/bin/python quantize_live.py --model-path /path/to/downloaded/model --output new-quantization-run.jsonTry a change and explain the result
Inspect each text’s likelihood before reading the aggregate. Decide whether this candidate should proceed to task evaluation. Explain what an independent code-completion evaluation set would need to check before release.
Read the explanation
Conversion success and quality acceptance are different. A small development set is still not a held-out task evaluation.
Choose a supported quantization path
Keep calibration/development and held-out evaluation distinct. Test the exact deployed quantization backend, dtype, and model revision. Check current supported APIs and migration guidance; do not ship a candidate merely because conversion completed.
References
- PyTorch quantization documentation and migration notice.
- vLLM quantization compatibility.
- Official model configuration.
Sources checked October 24, 2026. Runtime-specific commands and results apply to the recorded versions; linked rolling documentation can change. The application scenarios are teaching examples, not claims about an author’s production incident.