By Konic Labs, Inc.
TL;DR
We validate a from-scratch, pure-PyTorch AWQ implementation on Qwen3-8B: group-wise INT4 with per-channel AWQ scaling produces a 4.0× smaller model (13.9 GB → 3.5 GB linear weights) at 1.034× FP16 perplexity on WikiText-2 (10.08 vs 9.75), loaded and run in a real INT4 GEMM runtime. The decisive factor is norm-folding the AWQ scale — 20× more accurate per weight than weight-dequantization.
Activation-aware Weight Quantization (AWQ, Lin et al. 2023) is the de-facto method for turning a 7–8B-class FP16 LLM into a deployable INT4 model. It protects the weight channels aligned with large-magnitude activations by scaling them up before quantization, then compensating the activations. The production toolchain, however, is largely black-box - AutoAWQ and GPTQ packages that hide the scale search, the packing, and the runtime handoff behind a single call.
We wanted the opposite: a from-scratch, pure-PyTorch, model-agnostic implementation where every stage - activation statistics, per-layer scale search, INT4 packing, reconstruction verification, and the bridge to a real INT4 runtime - is inspectable. And we wanted to prove, end-to-end on a viable-sized model on CUDA, that it produces a 4× smaller model with near-zero loss.
This post presents that implementation and its validation on Qwen3-8B. The pipeline produces an INT4 model that is 4.0× smaller in linear weights and runs at 1.034× FP16 perplexity in a real INT4 GEMM runtime, with coherent greedy generation indistinguishable in substance from the FP16 baseline.
For a linear layer with weight and per-input-channel activation magnitude , AWQ chooses a per-channel scale
scales the weights up by before quantization (), and divides the activations by at inference. The reference implementation (mit-han-lab/llm-awq) folds into the preceding norm's weight () rather than storing per linear. The optimal is found by grid search, scored by the activation-weighted reconstruction error ; gives (plain RTN), so the search can only improve on RTN, never worsen it.
Weights are quantized in groups of group_size input channels with one FP16 scale per group: , , so the signed grid is (15 codes). Two INT4 are packed per byte in the internal artifact.
The runtime-loadable layout is unsigned int4 packed 8-per-int32, with an AWQ pack order governing the within-int32 ordering. Reconciling our per-linear with this shared-norm convention is the hard part - covered in §4.
The pipeline is deliberately linear and inspectable. Each phase is independently runnable and produces a discrete artifact on disk.
FP16 model ──► calibrate ──► calibration_stats.pt ──► scales ──► awq_scales.pt
│
▼
prompts ──────────────────────────────────────────────────► quantize ──► quantized_state.pt + metadata.json
│
▼
verify (MSE)
│
▼
export ──► awq_hf/ (AutoAWQ GEMM)
qweight / qzeros / scales
+ folded-norm FP16A forward hook on every nn.Linear aggregates the per-input-channel activation magnitude on-the-fly as a running sum: , accumulated per layer with a running count. Memory is per layer - not - so hundreds of calibration samples cost nothing beyond a single forward. We calibrate on 128 WikiText-2 samples at max_length = 2048.
For each linear, we grid-search over n_grid = 20 values in and score each candidate by actually quantizingthe weight with it (the pseudo-quantizer mirrors the real quantizer's grid exactly) and computing . The layer count for depth-dependent skip strategies is derived from the calibration stats via model.layers.{i}.*, not hardcoded - the implementation is model-agnostic for any Llama/Qwen/Mistral-style causal LM.
The quantizer never loads the full model. It streams safetensors one weight tensor at a time, applies , performs group-wise symmetric INT4 quantization, packs two INT4 per byte, and writes quantized_state.pt + metadata.json. Peak memory is one tensor plus its packed output. All 252 linear layers of Qwen3-8B are quantized this way.
The canonical dequantizer inverts the packer and reports MSE against the original FP16 weight read from disk. This is a reconstruction-quality check on the quantization itself, independent of any runtime.
The hard part is producing a model a real INT4 runtime can load, because our per-linear does not map 1:1 onto the standard AWQ runtime convention:
q_proj / k_proj / v_proj share input_layernorm;gate_proj / up_proj share post_attention_layernorm;o_proj and down_proj have no preceding norm at all.The exporter reconciles this as follows:
q/k/v/gate/up) with the shared .o_proj / down_proj as plain RTN INT4 () - they have no preceding norm to absorb a scale.The output directory contains AutoAWQ GEMM qweight / qzeros / scales per quantized linear, the FP16 (folded) norms + embeddings + lm_head, and a quantize_config.json. AutoAWQ loads it directly and replaces each quantized linear with a WQLinear INT4 GEMM module.
| Value | |
|---|---|
| Hardware | AWS EC2 g6.xlarge, NVIDIA L4 (24 GB) |
| Software | PyTorch 2.6.0+cu124, transformers 5.12.1, AutoAWQ 0.2.9 |
| Models | Qwen3-0.6B (neg. control), Qwen3-1.7B (primary), Qwen3-8B |
| Quantization | all linears, group_size = 128, 128 WikiText-2 samples, max_length = 2048 |
| Perplexity | WikiText-2 test split, sliding window (stride 512, max_length 2048) |
| Generation | greedy (do_sample=False), max_new_tokens = 48, fixed 4-prompt set |
The pass bar for near-zero loss is AWQ perplexity ≤ 1.10× FP16.
On the production-relevant 8B model, the exported AWQ-INT4 artifact is 4.0× smaller in linear weights (13.9 GB → 3.5 GB) and runs at 1.034× FP16 perplexity(9.75 → 10.08) in AutoAWQ's real INT4 GEMM runtime - comfortably under the 1.10× pass bar, with coherent greedy generation across every prompt.
| Model | FP16 PPL | AWQ-runtime PPL | × FP16 | Bar |
|---|---|---|---|---|
| Qwen3-0.6B (neg. control) | ~coherent | coherent | - | below INT4 viability |
| Qwen3-1.7B | 16.79 | 20.90 | 1.245× | INVESTIGATE |
| Qwen3-8B | 9.75 | 10.08 | 1.034× | PASS (≤1.10×) |
Headline perplexity on the WikiText-2 test split. AWQ-runtime = awq export → AutoAWQ real INT4 GEMM.
Benchmark comparison
WikiText-2 PPL (8B)
3% worse
Linear weights (GB)
75% better
Runtime model on disk (GB)
62% better
| Task | FP16 baseline | AWQ INT4 |
|---|---|---|
| WikiText-2 PPL (8B) | 9.75 | 10.08 |
| Linear weights (GB) | 13.9 | 3.5 |
| Runtime model on disk (GB) | 16 | 6.1 |
The compression ratio counts packed INT4 weights plus per-group FP16 scales and per-channel AWQ scales, so it reflects the true on-disk footprint of the quantized linear weights. Reconstruction MSE is tiny relative to the weight magnitudes - and, crucially, the runtime does not pay this error as a forward-pass penalty because the export folds into the norm (§7).
| Metric | FP16 | INT4 AWQ | Ratio |
|---|---|---|---|
| Linear weights (all 252, 36 layers) | 13.9 GB | 3.5 GB | 4.0× smaller (25.0%) |
| Runtime-loadable model on disk | 16.0 GB | 6.10 GB | 2.6× |
| Avg verify MSE (5 layers) | - | 2.59e-4 | - |
Qwen3-8B compression and reconstruction quality.
Across a fixed 4-prompt set, the INT4 model matches the FP16 baseline in substance - no degeneration or repetition. Qualitative near-zero loss.
| Prompt | FP16 | AWQ-runtime (INT4 GEMM) |
|---|---|---|
| "The capital of France is" | Paris, Rome, Madrid, Berlin, Amsterdam… | Paris, Washington DC, London, Berlin, Rome, Madrid… |
| "Explain gradient descent in one sentence:" | correct, coherent | correct, coherent |
| "def fibonacci(n):" | clean recursive fib | clean fib (if n <= 1) |
| "Once upon a time in a galaxy far away," | "planet Mathoria, base-12 number system" | "planet Mathoria, Number Navigators" |
Qwen3-8B greedy generation: FP16 vs AWQ-runtime (real INT4).
Quality retention improves with scale - 1.7B sits at 1.245× (the INVESTIGATE edge), while 8B passes cleanly at 1.034×. This is exactly what AWQ theory predicts: larger models have more redundant capacity to absorb fixed-point error. The 8B result, not the 1.7B one, is the deployment-relevant data point.
The single most important design choice is how the AWQ scale is applied at inference. There are two strategies:
mit-han-lab/llm-awq convention and what awq export does.They are not equivalent. On a single 1.7B projection layer:
| Strategy | Weight norm (orig 72.03) | MSE vs FP16 | Per-layer output rel. err. |
|---|---|---|---|
| Weight-dequant (Q(W·s)/s) | 79.38 (+10%) | 4.2e-4 | 29–46% |
| Norm-fold (Q(W·s), s in norm) | 72.27 (+0.3%) | 2.1e-5 | - |
model.layers.0.self_attn.q_proj (Qwen3-1.7B, group_size = 128).
| Model | FP16 | AWQ-runtime (norm-fold) | Weight-dequant (Q(W·s)/s) |
|---|---|---|---|
| Qwen3-1.7B | 16.79 | 20.90 | 264 120 |
| Qwen3-8B | 9.75 | 10.08 | 174 464 |
Full per-config perplexity. The weight-dequant column is the quantitative evidence that the strategy choice is decisive.
Grid search wins on per-layer weight reconstruction (~2× lower MSE), confirming the search is real and the scoring function is correctly weighted. Fixed- wins on end-to-end runtime PPL - and this is not a scoring bug.
| Scale selection (1.7B) | Verify MSE (weight) | AWQ-runtime PPL |
|---|---|---|
| Grid search (--n-grid 20) | 2.74e-4 | 20.90 |
| Fixed α = 0.5 (--no-grid-search) | 5.63e-4 | 19.94 |
Grid search wins on reconstruction; fixed-α wins on export runtime PPL.
v_proj / up_proj output channels) for further quality headroom.model.layers.{i}.* convention makes Llama/Mistral support straightforward. lm_head is intentionally skipped (vocabulary projection is too sensitive to INT4).A from-scratch, pure-PyTorch, model-agnostic AWQ implementation, with a runtime-loadable export, delivers a 4× smaller Qwen3-8B at 1.034× FP16 perplexity - near-zero loss, coherent generation, runnable in a real INT4 GEMM runtime. The decisive factor is norm-folding the AWQ scale (20× more accurate per weight than weight-dequantization), and the clear next step is norm-group-level scale search to bring smaller models into the same near-lossless regime.
The implementation is open-source at github.com/egesabanci/awq, with the full pipeline (calibrate / scales / quantize / verify / export) and a standalone evaluator for perplexity and generation.
All numbers here are regenerated by the commands below.
# Quantize (CUDA)
awq run --model /data/models/Qwen3-8B --dataset wikitext \
--output-dir /data/out-8b --samples 128 --max-length 2048 \
--device cuda --quantize-strategy all --group-size 128 --verify-layers 5
# Export to a runtime-loadable AutoAWQ/HF-AWQ model
awq export --model /data/models/Qwen3-8B \
--from /data/out-8b/awq_quantized/quantized_state.pt \
--to /data/out-8b/awq_hf --group-size 128 --device cuda
# Evaluate: FP16 baseline vs AWQ real-INT4 runtime
python eval/ppl.py --model /data/models/Qwen3-8B --config fp16
python eval/ppl.py --model /data/models/Qwen3-8B --config awq-runtime \
--export-dir /data/out-8b/awq_hf --genKey takeaways
4.0×
Smaller linear weights on Qwen3-8B
1.034×
FP16 perplexity (10.08 vs 9.75)
20×
More accurate per weight via norm-folding
0
Re-quantization error on AWQ-scaled linears
Author
Written by Konic Labs, Inc. — founders and research team. Open, reproducible research for on-prem enterprise LLMs.
Related guides
Data sovereignty is the reason most regulated enterprises cannot use hosted LLM APIs: the moment proprietary data crosses a network boundary, control over it is shared. This guide explains what data sovereignty means for AI, why it is driving on-prem adoption, and how to evaluate a deployment against your sovereignty requirements.
An air-gapped LLM runs on infrastructure with no connection to the public internet — the model, the data, and the serving stack all live inside your boundary. This guide explains what air-gapped deployment actually requires, which industries need it, and the practical constraints of running a model with no external dependencies.
Deploying an LLM on your own infrastructure is a sequence of concrete decisions: pick the model, size the hardware, choose a serving runtime, quantize for your GPU, and wire in monitoring. This guide walks each step with the trade-offs that actually matter for a production on-prem deployment.
More research
We surgically compress Liquid AI’s LFM2.5-VL-3B — an architecture-searched hybrid conv+attention vision-language model — by cutting FFN width where the search never optimized it: joint-SwiGLU Wanda pruning (10752→8192 and →7168), distillation-to-baseline LoRA recovery, and hand-rolled W4A16 GPTQ + W8 mixed-precision quantization. All three tiers are near-lossless against the base on our paired eval shards — PPL ratios at or below baseline (0.79–0.85), top-5 token agreement ≥ 0.95, quantization adding only +0.02–0.06 nats — across 5.30 GB (−15%), 4.92 GB (−21%), and 1.93 GB packed / 2.22 GB (Konic Optimized) — native INT4/INT8 compressed-tensors in vLLM (−69%). Live comparison on the smallest tier preserves scenes, facts, and tool calling — 10/10 tool rounds, including correct abstention where the base over-triggered. Recorded runs, not general benchmarks.
We augment Liquid AI’s LFM2.5 Encoder 230M — a 230M-parameter bidirectional masked-language encoder — with a SigLIP2 vision tower and 32 learned soft tokens to build an encoder-only multimodal model for retrieval and image-text matching. Clean image-only retrieval on 12,500 held-out MONET pairs reaches 0.1194 image→text R@1 with the BF16 reference; the GPTQ INT4 release retains 0.1091 while shrinking the package from 923.65 MB to 370.46 MB (−59.9%). Cyclic-negative matching AUROC is 0.9733; text-nearest hard negatives and image-conditioned masked-token prediction remain at chance, and the model does not generate.
We chain two complementary compression stages on Liquid LFM2.5-8B-A1B — REAP CUDA expert pruning (32→16 experts across 22 MoE layers) then external AWQ INT4 quantization (W4A16_ASYM, group 128). The published packed artifact is 1.15B packed-weight equivalent / 2.79 GB; recorded MATH500 and BFCLv3 results use a separate 9.18 GB AWQ-scaled BF16 derivative for vLLM evaluation, not direct packed-INT4 runtime. These are recorded run summaries, not general benchmarks.
We present REAP-MLX, an Apple Silicon implementation of Router-weighted Expert Activation Pruning (REAP) for Mixture-of-Experts language models. We evaluate quality retention across compression ratios on the LFM2.5-8B architecture: REAP preserves 96.8% of code generation performance at 25% compression and 91.4% at 50% compression, with less than 0.4 percentage point variance across independent calibration draws.