Files
vllm-omni/probe/issue-5/FINDINGS.md
T
admin e37c5aed40
Build Wheel / build (3.12) (pull_request) Failing after 36s
Build Wheel / build (3.11) (pull_request) Failing after 49s
pre-commit / pre-commit (pull_request) Failing after 44s
probe(evidence): round two - W8A8 int8-dynamic passes the structure gate at BF16-parity PSNR; verdict flips to YES; upstream Marlin report draft staged
2026-10-08 09:06:49 -04:00

247 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Issue 5 probe: int8 quantized DiT serve paths on the RTX 3090 (24 GB)
## Round two (owner directive + amendment): W8A8 int8-dynamic — the capacity route is YES
The 08:37 amendment narrowed the remaining work to one decisive experiment:
a compressed-tensors **W8A8 int8-dynamic** artifact (int8 weights + dynamic
per-token symmetric int8 activations, int-quantized format) whose kernel on
sm_86 is the cutlass int8 scaled-MM — no Marlin anywhere in the path.
**Result: the non-Marlin int8 path works end-to-end.** The artifact serves,
passes the structure gate on all 16 matrix requests with healthy numbers,
and lands at BF16-parity PSNR — including the first usable four-reference
quality comparison this card has ever produced:
| Refs | Wall (mean, s) | Peak VRAM | Opacity / alpha | PSNR vs sealed Diffusers (dB) |
| --- | --- | --- | --- | --- |
| 1 | 20.5 | 23,879 MiB (run max) | 0.984–0.986 / 255.0 | 33.72 |
| 2 | 25.1 | ” | 0.947–0.966 / 254.9–255.0 | 28.34 |
| 3 | 29.4 | ” | 0.965–0.988 / 255.0 | (no sealed row; derived fixture) |
| 4 | 34.0 | ” | 0.967–0.976 / 255.0 | **27.50** |
Comparison rows: BF16 served 1–2 refs at 34.2/36.7 s and OOMs at 4;
online-FP8 and int8-wo-Marlin run 29.9–46.4 s but produce collapsed output;
**W8A8-dynamic is both the fastest row measured (4-ref 34.0 s ≈ 4.3× the
sealed Diffusers nocache baseline's 145.8 s) and the only structurally sound
quantized row.** 3–4 references at cached speed on one 3090: delivered.
Gate calibration note: round one's pass rule (`opaque ≥ 0.99`) was an
instrument calibration derived from the broken (0.008–0.20) vs BF16 (1.0)
contrast. The W8A8 outputs sit at 0.95–0.99 opaque with alpha mean ≈255 and
healthy RGB spread — soft-edge alpha, not the broken signature — so the
recorded verdict rests on the raw numbers, visual inspection (four-character
apothecary scene and the 1-ref portrait both render faithfully), and PSNR in
the 26–29 dB BF16 band. The raw per-request structure metrics are in
`evidence/rtx3090-round2/` unedited.
Kernel-path verification for this round (`w8a8_isolate.py`,
`cutlass_ops_probe.py`): the raw ops (`scaled_int8_quant` +
`cutlass_scaled_mm`) match a float32 reference at 0.17% rel-L2 across
(64,256,128) / (512,4096,4096) / (512,4096,12288), and the full layer path
(load → transpose-repack → apply, fused QKV and row-parallel, synthetic
weights, loaded bitwise-verified) matches at 0.17% too. (Round-two lesson
recorded for the instrument: an intermediate 97%-error scare in
`w8a8_isolate.py` was a missing ×act_scale factor in the probe's own
reference, not a kernel defect — the out/ref norm ratio equaled E[act_scale]
exactly, which is what gave it away.)
Artifacts (outside Git): `…/artifacts/qwen-image-2.1-dit-w8a8dyn/` (int8
weights unpacked + fp32 per-channel scales, 224 tensors, 7.26 GiB, manifest
with digests). The round-one int8-wo artifact stays for the Marlin evidence.
The upstream Marlin report is drafted and ready to file in the #3 lane on
the owner's call: `UPSTREAM-REPORT-DRAFT.md` (capability-query overclaim +
NaN kernel + synthetic repro + the PR #4 FP8 convergence). The hard-stop
branch did not trigger.
Helper-system follow-through for multi-model-helper-server #27 (input only):
the 3090's serving-profile candidacy now extends to 3–4-reference profiles
via the W8A8-dynamic artifact — 4-ref at 34.0 s/image, peak 23.9 GiB,
BF16-parity PSNR (27.5 dB 4-ref vs the sealed int8-DiT Diffusers baseline),
provided serving ships the artifact and the branch's two loader/factory
fixes. This supersedes round one's "blocked on Marlin" conclusion.
## Round one: int8 weight-only via compressed-tensors — capacity yes, Marlin W8A16 collapses
Probed 2026-10-08 on `cachyos-alexander` (RTX 3090, sm_86, driver 615.71.09,
CUDA 13.4), at upstream main `8484d85d1` plus this branch's two loader/factory
fixes, vLLM-Omni 0.31.0 recipe (Python 3.13 venv, torch 2.13.0+cu130,
transformers 5.14.1, diffusers git `122b1e11`, compressed-tensors 0.17.0).
The DiT was quantized **offline** from the sealed snapshot
`Qwen/Qwen-Image-2.1@790c9263` to int8 weight-only (W8A16), per-output-channel
symmetric, stored as a compressed-tensors pack-quantized checkpoint; encoder
and VAE stay BF16 on disk via the artifact's per-component config wrapper.
The probe mirrors the sealed Diffusers attempt `2026-10-07T170729Z-q3-4r`
fixture-for-fixture (same frozen references-suite-2 images, same rendered
prompts, seeds 2301–2304, 20 steps, `true_cfg_scale 1.0`).
## Verdict (superseded by round two above — kept as the round-one record)
**Capacity: delivered. Quality: collapsed in the serve kernel — the int8
sidestep does not rescue 3–4 references on this card with this stack, and the
blocker is now sharply isolated: the 8-bit Marlin W8A16 path itself.**
- Every step of the artifact chain is verified correct: the quantizer's
round-trip reproduces the sealed BF16 weights at ~1.1% rel-L2
(`evidence/rtx3090/artifact-verify.txt`), and the serve path loads the
packed tensors **bitwise-identically** — including the fused q/k/v stacked
shards — with finite scales before and after the Marlin repack.
- Serving works mechanically: init ~40 s, all 16 capacity-matrix requests
(1/2/3/4 references × seeds 2301–2304) complete at BF16-comparable speed
(4-ref 46.4 s; BF16 OOMs at 4 refs, online-FP8 was 44.3 s), peak 23,935 MiB.
- But the output is structurally broken on **every** counted run (gate FAIL
16/16: opaque 0.0–0.20 vs 1.0 at BF16; PSNR vs the sealed baseline
7.1–7.5 dB — the same collapsed band as online FP8). The images are
horizontal-stripe noise.
- A one-layer offline isolation (`marlin_isolate.py`) pins it: vLLM parallel
linears built exactly as the qwen_image_21 transformer builds them, loaded
with the artifact's verified tensors, emit **NaN** for all four layer
geometries (fused QKV, to_out, img_mlp proj/out), for both bf16 and fp16
activations — and do the same for **synthetic random int8 weights**, so the
checkpoint and its quantization are exonerated. The 8-bit Marlin W8A16 path
on this stack (sm_86, vllm 0.31.0, torch 2.13.0+cu130) is broken even though
`query_marlin_supported_quant_types` accepts `uint8b128` on the card, and
`choose_mp_linear_kernel` offers no non-Marlin W8A16 fallback on sm_86
(Machete is sm90+, ExLlama int4-only).
The torchao-int8-in-Diffusers prior (multi-model-helper-server #160, blinded
review within label noise of BF16) stands unchallenged: int8 quantization is
fine; the serve-path kernel is not. The online-FP8 collapse from PR #4 and
this int8 collapse now share one root suspect — the Marlin mixed-precision
path on this fork/card — with the fp8 case previously isolated to the
Marlin-quantized DiT and this run extending it to int8 with a fully verified
checkpoint on the far side of the kernel.
## Two serve-path fixes this issue required (in this PR)
1. **Quant factory could not build compressed-tensors at all.**
`_normalize_method_name` canonicalizes `compressed-tensors` to
`compressed_tensors`, which then missed vLLM's hyphenated registry — so
both `--quantization compressed-tensors` and checkpoint auto-detection
raised "Unknown quantization method". The registry lookup now resolves
both spellings, and registry configs whose `__init__` does not take the
serialized schema fall back to `from_config` (`vllm_omni/quantization/factory.py`,
tests in `tests/quantization/test_quant_factory.py`).
2. **The diffusion loader misclassified pre-quantized checkpoints as online
quantization under CPU offload.** With `--enable-cpu-offload` it loaded
every component on the GPU before offloading (encoder 17 GiB + VAE + the
packed DiT skeleton > 24 GiB → init OOM). Serialized formats are now
detected (`is_prequantized_config`, component-wrapper aware) and load
directly on the host, with each quantized module round-tripped through the
accelerator only for the post-load Marlin repack
(`vllm_omni/diffusion/model_loader/diffusers_loader.py`). With these two
fixes the artifact serves: init healthy in ~40 s, logs
`Per-component quantization: {'transformer': 'compressed-tensors'}`.
## Artifact (outside Git, under the helper state root)
`~/.local/state/multi-model-helper-server/state/image-service/artifacts/qwen-image-2.1-dit-int8wo/`
— quantized `transformer/` (6.76 GiB packed int8 + BF16 boundary layers;
224 tensors = 32 blocks × {to_q,to_k,to_v,to_out.0,proj,gate_layer,out};
6.98 B params quantized, 0.14 B BF16), `model_index.json`/`scheduler`/
`text_encoder`/`vae`/`processor` symlinked to the sealed snapshot, and
`MANIFEST.json` with inputs (snapshot path, revision, tree digest), versions,
per-file sha256, and quantization stats (mean rel-L2 1.10%, max 1.73%).
Built by `quantize_dit_int8wo.py` using compressed-tensors' own
`pack_to_int32` (uint8 biased by 128, 4 per int32) and `QuantizationConfig`
serialization, so the storage convention matches llm-compressor bit-for-bit.
`transformer/config.json` carries the quantization metadata wrapped
per-component (`{"transformer": {...}}`) so auto-detection quantizes the DiT
only — the encoder and VAE resolve to None and stay BF16
(the plain global form would hand the encoder a compressed-tensors config
it has no scales for).
## Run matrix — RTX 3090
Single run `int8wo`: `vllm serve <artifact> --omni --port 8091
--served-model-name Qwen/Qwen-Image-2.1 --enforce-eager
--enable-cpu-offload`, readiness via `/health`, VRAM via the 1 Hz
`nvidia-smi` series (issue 2's instrument). Two precursor runs OOM'd at init
for reasons fixed on this branch (see Verdict item 2); their logs survive
under `runs/int8wo-expandable/` on the host (the first attempt's log was
overwritten by the successful rerun in the same label).
| Refs | Fixture | Wall (mean, s) | Peak (MiB) | Gate | PSNR vs Diffusers (dB) |
| --- | --- | --- | --- | --- | --- |
| 1 | edit-expression-ysolde | 31.0 | 23,935 (run max) | FAIL 4/4 | 7.14 |
| 2 | edit-compose-pair | 36.6 | ” | FAIL 4/4 | 7.46 |
| 3 | edit-compose-three | 41.4 | ” | FAIL 4/4 | no sealed row (derived fixture) |
| 4 | edit-compose-four | 46.4 | ” | FAIL 4/4 | 7.11 |
BF16 comparison rows (issue 2): 1-ref 34.2 s, 2-ref 36.7 s, 4-ref OOM.
Online-fp8 (broken output, capacity reference): 29.9 / 34.1 / 38.9 / 44.3 s.
So the int8 artifact matches the fp8 capacity/speed profile exactly —
including its broken-output signature (PSNR band 7.0–7.5 dB) — while the
checkpoint side is proven numerically sound, moving the defect wholly into
the kernel.
## Structure gate and quality read
- Gate rule (issue 1's lesson): `opaque_fraction ≥ 0.99` and `mean(rgb_std)
≥ 10`; BF16 outputs measure 1.0 opaque, 26–29 dB. Every int8-artifact run
fails (opaque 0.0–0.20, alpha mean 114–137; rgb_std spans 3–93 — several
outputs have healthy RGB spread yet near-total transparency, and one
(seed 2302) is near-flat grey like the fp8 signature). Outputs are
horizontal-stripe noise on visual inspection.
- PSNR vs the sealed 4-reference Diffusers baseline (RGBA-on-RGBA,
#1's comparator): 7.14 / 7.46 / 7.11 dB mean for 1/2/4 refs — the
collapsed band, not the 26–29 dB BF16-row parity the issue hoped for.
## Kernel isolation (the issue's "different kernels" prior, answered)
`marlin_isolate.py` builds vLLM `QKVParallelLinear` / `RowParallelLinear` /
`ColumnParallelLinear` exactly as `qwen_image_21_transformer.py` does, loads
the artifact tensors through the production weight-loader machinery
(bitwise-verified after load, fused q/k/v shards included), runs
`process_weights_after_loading` (the Marlin repack) on the GPU, and compares
`layer(x)` against `float32 x @ dequant(W)ᵀ` computed from the artifact's own
unpacked weights:
| Layer | bf16 acts | fp16 acts | packed bitwise | scales post-repack |
| --- | --- | --- | --- | --- |
| attn.to_qkv (fused) | NaN | NaN | equal | finite (6.6e-4…1.2e-2) |
| attn.to_out.0 (row) | NaN | NaN | equal | finite |
| img_mlp.proj (column) | NaN | NaN | equal | finite |
| img_mlp.out (row, wide-in) | NaN | NaN | equal | finite |
Synthetic random int8 weights reproduce the NaN identically, and the runtime
capability query accepts `uint8b128` on this card — so this is a defect of
the 8-bit Marlin W8A16 path on this stack (repack or kernel; sm_86,
vllm 0.31.0, torch 2.13.0+cu130), not of the checkpoint, the quantizer, the
loader, or the fused-QKV stacking. No alternative W8A16 kernel exists on
sm_86 in this build to switch to.
## Reproducibility
Scripts in this directory: `quantize_dit_int8wo.py` (artifact + manifest),
`verify_artifact.py` (round-trip check), `serve.sh` / `probe.py` (adapted
issue-2 kit; probe records the structure gate inline per request), `gate.py`
(gate + capacity summarizer), `psnr.py`, `marlin_isolate.py` (kernel
isolation, `--synthetic` for the stack-only variant). All were pushed
verbatim to `~/Projects/vllm-omni-val/issue5/` on the host and run from the
validation clone checked out on this branch. Per-run serve/vram logs and all
output PNGs live under `~/Projects/vllm-omni-val/issue5/runs/int8wo/`;
condensed evidence in `evidence/rtx3090/`.
## Helper-system follow-through (input to multi-model-helper-server #27 — not implemented here)
- The 3090's 3–4 reference serving path remains blocked, now with a single
named suspect: the 8-bit Marlin W8A16 kernel path on sm_86 (this fork's
vllm 0.31.0 / torch 2.13.0 stack). Both quantized routes (online FP8,
offline int8 compressed-tensors with a verified checkpoint) collapse at the
same place. The unlock order for #27 candidacy is now: upstream Marlin
8-bit fix (or a non-Marlin W8A16 kernel on sm_86), then serving — capacity
and speed are already proven sufficient (4-ref at 46.4 s, 2.9× the sealed
Diffusers nocache baseline).
- The two vllm-omni loader/factory fixes on this branch are prerequisites for
any compressed-tensors artifact serving under CPU offload and are
independent of the kernel defect.
## Out-of-scope confirmation
No upstream PRs; no W8A8/int4/NVFP4 artifacts built; no helper-system
deployment evidence. Root-causing the Marlin 8-bit defect remains the
separate lane (PR #4 carries the fp8 isolation; this branch adds the int8 +
synthetic + bitwise-load isolation). The only code changes are the two
serve-path fixes required to make the issue's plan steps executable at all.