Skip to content

Phase 5a: int8 weight-only quantization - #5

Merged
bsreecharanreddy merged 18 commits into
mainfrom
phase-5a-quantization
Sep 17, 2026
Merged

bsreecharanreddy merged 18 commits into
mainfrom
phase-5a-quantization

Conversation

@bsreecharanreddy

Copy link
Copy Markdown
Owner

Summary

  • Designs and implements Phase 5a: self-computed, per-output-channel int8 weight-only quantization extending the Triton grouped-GEMM kernel itself (src/dispatch/kernels/quantization.py, grouped_gemm_int8.py) -- not sourced from bitsandbytes/AWQ, per the design doc's explicit scope decision.
  • Kernel-level correctness gate passed on a real NVIDIA L40 (RunPod Secure Cloud): the int8 kernel's first real-hardware run, 15/15 GPU tests against an independent quantize-then-dequantize reference; the existing bf16 kernels re-verified at 25/25.
  • Measured on the real deepseek-ai/deepseek-moe-16b-base model (bf16, single L40, unbatched eager decode): int8 kernel +70.9% over stock -- a throughput tie with the bf16 naive kernel it's built on (-1.2%), as expected since weight-only quantization saves memory bandwidth, not FLOPs. 49.89% expert-weight memory reduction. Perfect model-level top-1/mutual-top-k agreement at every tested position, stronger than the design doc's own bar required.
  • Three real bugs found and fixed, none caught by earlier per-task review:
    • Found live during the GPU rental: patch_moe_infer_quantized quantized each layer's weights into int8 but never freed the original bf16 weights (stack_expert_weights re-points every expert's Linear.weight at a view into a shared bf16 tensor), so the model held both copies at once and OOMed a 44GB L40. Fixed with _free_expert_weights plus a regression test (and, after the final review flagged it as insufficient, a second test using a weakref to prove the underlying tensor is actually collected, not just the views).
    • Found by the final whole-branch review: quantize_per_channel_int8 computed its arithmetic in the weight's own bf16/fp16 dtype rather than float32, widening round-trip error (bf16: up to 1.5x the ideal bound).
    • Found by the same review: the zero-channel NaN guard clamped every small-magnitude channel's scale toward a dtype-dependent floor, not just the all-zero case -- for fp16 specifically, this silently threw away int8 range on real, nonzero, CLI-reachable data (--dtype float16 --moe-kernel quantized).
  • One gap recorded honestly rather than hidden: the design doc's mutation check on the kernel (the same bar Phase 1 met) wasn't run this session -- documented as deferred, not fabricated.
  • README, CLAUDE.md, and CHANGELOG.md all refreshed -- CLAUDE.md and CHANGELOG.md had drifted stale since Phase 1 and were caught by the final review.
  • $1.9543 of the $5 cap, across three pods including two RunPod Community Cloud pods that hit a real host-level GPU passthrough bug (cuInit() returning CUDA_ERROR_UNKNOWN even against the base image's own stock torch) before a Secure Cloud pod worked.

Test plan

  • make check green (lint, mypy --strict, 123 tests) throughout every commit
  • Kernel-level correctness gate passed on real hardware: 15/15 int8, 25/25 bf16
  • Model-level agreement measured and reported exactly as it came back (perfect, not assumed)
  • Two real precision bugs found by the final whole-branch review, both fixed with regression tests proving they have teeth
  • Findings, cost records (all three pods), memory-footprint measurement, and runbook committed: docs/findings/2026-09-17-phase-5a-quantization-run.md

Splits the design doc's bundled Phase 5 (quantization + speculative
decoding) into two sequential sub-phases per brainstorming decision.
5a scopes an int8 weight-only grouped-GEMM kernel extension; 5b
(speculative decoding) is planned separately once 5a merges.
…ate scale shape

Hoist the per-output-channel scale multiply outside the K-loop instead of
dequantizing the weight tile every step: tl.dot now runs on x_tile's
native bf16/fp16 dtype (int8 -> that dtype is exact) instead of forcing
fp32/TF32-rate tl.dot, which would have made the kernel's throughput
measurement conflate quantization's real speedup with a dot-precision
regression. Also reject a QuantizedTensor whose scale shape doesn't match
weight (E, N), since the kernel's masked scale load trusts data's N
without checking scale's own shape.
stack_expert_weights re-points every expert's Linear at a view into one
shared bf16 tensor rather than copying it. patch_moe_infer_quantized
quantized that stack but never dropped those views, so the model held
both the bf16 originals and their int8 copies at once for every layer --
defeating quantization's whole point, and OOMing the real Phase 5a
measured run on a 44GB L40 at DeepSeekMoE-16B's scale.
Kernel-level correctness gate passed on real hardware (15/15 int8, 25/25
bf16). Measured three-way run: naive bf16 kernel +72.9% over stock,
int8 quantized +70.9% over stock but a throughput tie with naive
(expected -- weight-only quantization saves memory, not FLOPs). Memory
footprint measured directly: 49.89% reduction. Model-level agreement
(quantized vs. naive) came back perfect at every tested position,
stronger than the design doc's own bar required. Total cost $1.9543 of
the $5 cap across all three pods, including two Community Cloud pods
that hit a real host-level GPU passthrough bug.
Added the sections almanac's README has that this one was missing
(In sixty seconds, Why this project, The centerpiece, an architecture
diagram, a measured-results table per phase, what it doesn't do,
stack/layout tables, a paper-trail index) and fixed a stale line
claiming Phase 4's PR was still open after it had already merged.
grouped_matmul_int8's docstring still described the pre-fix-round-1
scale timing ("before accumulating"); integration.py's quantized patch
path was missing the type: ignore explanation its sibling function
already carries. Both flagged in the SDD ledger for the final review;
fixed directly since they were already known and trivial.
…cliff

quantize_per_channel_int8 was computing absmax/scale/quotient in the
input weight's own dtype. For bf16 that widens round-trip error to
~1.5x the ideal 0.5-quantization-step bound; for fp16, clamping the
zero-channel guard at fp16's own tiny() (~6.1e-5) silently clamped every
small-but-nonzero channel's scale upward too, cutting a channel with
absmax ~0.001 down to an int8 code of ~16 instead of ~127. Found by the
final whole-branch review, not caught by any prior task review since
the existing round-trip test only ran fp32 and the zero-channel tests
only checked for NaN, not range utilization.

Now computes absmax/scale/quotient in float32 regardless of weight's
dtype, and the zero-channel guard sets scale to 1.0 only when absmax is
exactly zero rather than clamping every small channel toward a
dtype-dependent floor. Round-trip test now parametrized over
float32/bfloat16/float16 with one bound that must hold for all three;
new regression test proves a small-but-nonzero fp16 channel still uses
the full int8 range.
- README: fix PR-number misattribution (Phase 2 landed via PR #3
  alongside Phase 3, not its own PR) and an off-by-one session count.
- CLAUDE.md: refresh "Current status" past Phase 1 -- it hadn't been
  touched since, so it silently described a four-phase-old project.
- CHANGELOG.md: same staleness, same fix, phases 2 through 5a added.
- findings doc: record honestly that Phase 5a's own mutation check
  (named in the design doc, met by Phase 1) wasn't run this session;
  write up the tl.dot precision finding that previously lived only in
  a source docstring; note that quantizing doesn't reduce peak GPU
  memory, only steady-state footprint.
- memory-footprint.json: add model/dtype/device, matching every other
  Phase 5a results JSON's own convention of carrying its full config.
- backends.py: add QUANTIZED_BACKEND, used from run_baseline.py instead
  of two independent "quantized" string literals.
- run_kernel_bench.py: note the int8 kernel has no micro-benchmark path
  by design, rather than leaving it to inference.
- test_integration.py: add a weakref-based test proving the shared bf16
  tensor stack_expert_weights builds is actually collected after
  patch_moe_infer_quantized, not just that the views into it are gone.
The re-review caught two slips in the previous fix-round commit: the
new Phase 5a/4/3/2 entries landed after the pre-existing Phase 1 entry
instead of above it, breaking CHANGELOG's own newest-first convention;
and both it and README still said 119/122 tests after the same commit
had already added a test, landing on 123.
@bsreecharanreddy
bsreecharanreddy merged commit 70ceee5 into main Sep 17, 2026
1 check passed
@bsreecharanreddy
bsreecharanreddy deleted the phase-5a-quantization branch September 17, 2026 15:55
bsreecharanreddy added a commit that referenced this pull request Sep 18, 2026
…gures

README and CLAUDE.md now reflect Phase 5a merged (PR #5), Phase 5b
complete (PR #6), the per-phase findings folders, 170 tests, and the
running GPU cost ($39.95 across nine sessions). CLAUDE.md's cost
discipline gains three rules that each came from a real Phase 5b
incident: never leave a pod running across an unbounded wait, pull
evidence off the pod before stop, and treat stop as possibly final.

Also corrects errors found while re-verifying the Phase 5b findings
against the committed results JSONs: the prompt-lookup gate row had
mixed in the k-sweep run's numbers (the gate run matched 2 of 4 prompts
at 17.7% acceptance and 47.21 tok/s, not 3 of 4 at 14.9% and 44.74);
match counts were stated out of 32 instead of 16 (draft-model 13/16,
prompt-lookup 9/16); prompt_000 diverges in 6 of 8 sweep configs, not
7; and the summary claimed both drafters beat baseline when draft-model
is below it at every k. Adds the direct evidence that the same k=4
prompt-lookup config matched 2/4 prompts in one process and 3/4 in
another.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant