INC4AI's picture
Update README.md
33c41b1 verified
|
Raw History Blame Contribute Delete
13.1 kB
# HunyuanImage-3.0-Instruct-Distil Β· MXFP4 mixed-precision (`auto-round` / vLLM INC path)
This is the **`--format auto_round`**
the MXFP4-for-experts / MXFP8-for-everything-else recipe, exporting
`quant_method = "auto-round"` so vLLM loads it through the **INC** dispatch instead of the
`compressed-tensors` one. AR-only, DiT-only and AR+DiT were all run successfully on
vLLM 0.29.0 + vLLM-Omni `main`.
![AR+DiT full-pipeline output](images/ar_dit_full_tp1x1.png)
> Generated output, not a ground-truth reference: AR+DiT on **2 GPUs** (AR TP1 + DiT TP1),
> `seed=42`, 8 steps, prompt `A cute cat`.
---
## ⚠️ Read this first
1. **On Hopper (SM90: H100/H200) this is a memory saving, not a speed saving.** There are no FP4
tensor cores, so the experts run through Marlin as **weight-only FP4 (effectively W4A16)**:
```
Using MarlinExperts (weight-only FP4) for AutoRound MXFP4 MoE
Using MarlinMxfp8LinearKernel for MXFP8 GEMM
```
Blackwell (`b12x` / CUTLASS MXFP4) paths exist in vLLM but are **untested** for this build.
---
## Overview
| Field | Value |
|---|---|
| Base model | `tencent/HunyuanImage-3.0-Instruct-Distil` (`cfg_distilled=true`, `use_meanflow=true`) |
| MoE geometry | 32 layers Γ— 64 routed experts, `moe_topk=8`, 1 shared expert/layer, hidden 4096, `moe_intermediate=3072` |
| Scheme | **Mixed**: routed experts β†’ **MXFP4** (E2M1, group 32, E8M0); all other quantized `Linear` β†’ **MXFP8** (E4M3, group 32, E8M0) |
| Export format | **`--format auto_round`** β†’ `quant_method = "auto-round"`, `packing_format = "auto_round:llm_compressor"` |
| Loader path | vLLM **INC** (`INCConfig.override_quantization_method()` maps `"auto-round"` β†’ `"inc"`) |
| Quantization tool | auto-round **0.15.0**, `--model_free` (no calibration data); wall time **707 s** on 1 GPU |
| Disk size | **52.59 GB** (48.98 GiB) β€” MXFP8-ct build 91.25 GB, BF16 base 158 GB β‡’ **0.33Γ— BF16** |
| Parameters | total **83.04 B**: routed experts **77.31 B (93.1 %) @4-bit**, `self_attn` 1.49 B + `shared_mlp` 1.21 B @8-bit, **3.03 B BF16/FP32** |
| Expert tensor layout | `…experts.N.{gate_and_up_proj,down_proj}.weight_packed` = `uint8 [N, K/2]` (two E2M1 per byte, **low nibble = even column**) + `weight_scale` = `uint8 [N, K/32]` (E8M0: `value = 2^(byteβˆ’127)`) |
| Kept in BF16 / FP32 | ViT (`vision_model`, BF16), VAE (FP32, as in the base model), `wte`/`lm_head`, `guidance_emb`, `timestep_emb`, `timestep_r_emb`, `patch_embed`, `final_layer`, `time_embed*`, `vision_aligner`, and the **MoE router of all 32 blocks** (`…mlp.gate.wg`) |
`quantization_config` from `config.json` (verbatim, minus the long `extra_config`):
```json
{
"quant_method": "auto-round",
"packing_format": "auto_round:llm_compressor",
"bits": 8, "group_size": 32, "sym": true, "data_type": "mx_fp",
"act_bits": 8, "act_data_type": "mx_fp", "act_dynamic": true, "act_group_size": 32, "act_sym": true,
"model_free": true, "iters": 0, "enable_quanted_input": false,
"autoround_version": "0.15.0",
"block_name_to_quantize": "model.layers",
"extra_config": {
".*mlp\\.experts": { "bits": 4 }, // ← the fix; stock export has ".mlp.experts."
"final_layer.model.0.emb_layers.1": { "bits": 16, "data_type": "float", "act_bits": 16, "act_data_type": "float" },
"... 220 more entries, all bits:16 overrides ..."
}
}
```
Note there is **no `ignore` list** in this format (unlike the `compressed-tensors` sibling): the
16-bit exceptions are expressed as `extra_config` entries with `bits: 16`, and the non-Linear modules
that never quantize anyway (VAE convolutions, ViT patch/position embeddings) simply are not listed.
> `act_*` fields describe the *declared* scheme. What actually runs on SM90: dynamic MXFP8 activation
> quantization for dense `Linear`, but **no activation quantization for the experts** (Marlin W4A16).
---
## Quantization command
```bash
auto-round \
--model_name tencent/HunyuanImage-3.0-Instruct-Distil \
--model_free \
--scheme MXFP8 \
--layer_config '{.mlp.experts.:{bits:4,data_type:mx_fp}}' \
--ignore_layers "vision,guidance_emb,timestep_emb,timestep_r_emb,final_layer,wte" \
--format auto_round \
--device cuda:0 \
--output_dir ./HunyuanImage-3.0-Instruct-Distil-MXFP4-AutoRound
```
- Do **not** add a bare `gate` to `--ignore_layers`: it expands to `.*gate.*` and silently swallows the
fused expert projections `gate_and_up_proj` (4096 tensors).
- The MoE router needs no entry: AutoRound hard-skips anything matching `.gate.`.
- `vision` must stay ignored: ViT `mlp.fc2` input dim 4304 is not divisible by the 32-element MX block.
- The run log echoes `Scheme: … bits=8` / `Packing format: mxfp8-quantized` β€” that is the **default**
scheme only, not evidence that `--layer_config` was ignored. Check the artifact instead
(`weight_packed` count must be 4096).
---
## Inference environment
| Component | Version |
|---|---|
| vLLM | **0.29.0** |
| vLLM-Omni | latest `main` (validated at `1c7476ec`, `0.29.0rc2.dev161+g1c7476ec1`, editable) |
| PyTorch | 2.13.0+cu132 |
| FlashInfer | 0.6.18 |
| GPU | NVIDIA H200 141 GB β€” AR-only 1Γ—, DiT-only 2Γ—, AR+DiT 2Γ— |
```bash
pip install vllm==0.29.0 && pip install -e /path/to/vllm-omni
python -c "import vllm, vllm_omni, os; print(vllm.__version__, os.path.dirname(vllm_omni.__file__))"
# host-specific, drop if unneeded here
export NCCL_NVLS_ENABLE=0
export VLLM_USE_FLASHINFER_SAMPLER=0
cd /tmp # cwd must be neutral: children re-import vllm_omni from cwd
```
---
## Validated run modes (2 GPUs, cards 0 and 1)
| Mode | deploy YAML | Result | Evidence from logs |
|---|---|---|---|
| AR-only | `hunyuan_image3_ar_tp1.yaml` (TP1) | βœ… | `Model loading took 46.69 GiB`; MXFP4 MoE dispatch line above; text `It's a warm and healing style` (matches the `-mixed` and MXFP8 builds verbatim) |
| DiT-only | `hunyuan_image3_dit_tp2.yaml` (TP2) | βœ… image | `Model loading took 25.3899 GiB`/card; `Saved generated image …` (std 49.34) |
| AR+DiT | `hunyuan_image_3_moe_2gpu_tp1.yaml` (TP1+TP1) | βœ… image | both stages loaded (46.69 + 46.63 GiB); `Saved generated image …` (std 46.44) |
Before the metadata fix, **all three** failed with the `KeyError` quoted at the top.
```bash
export CUDA_VISIBLE_DEVICES=0,1
cd /tmp
python /path/to/vllm-omni/examples/offline_inference/text_to_image/text_to_image.py \
--model /path/to/HunyuanImage-3.0-Instruct-Distil-MXFP4-ar \
--deploy-config ./hunyuan_image_3_moe_2gpu_tp1.yaml \
--prompt "A cute cat" \
--num-inference-steps 8 \
--guidance-scale 5.0 \
--seed 42 \
--output ./output.png
```
The two-stage YAML (`stage 0` AR with `omni_kv_config.need_send_cache`, `stage 1` DiT with
`need_recv_cache`, joined by a `SharedMemoryConnector`) is reproduced in the sibling build's card;
`devices` are **local** indices, combine with `CUDA_VISIBLE_DEVICES`.
**Parameter notes** β€” `--num-inference-steps 8` (distilled: do not use 50); `--guidance-scale` is a real
input (`cfg_distilled=true`, vLLM-Omni feeds `1000 Γ— guidance_scale` as the guidance embedding; the image
above used `5.0`, vLLM-Omni's Distil e2e reference uses `2.5`); `--prompt` reaches only the AR stage.
---
## Fidelity
### Weight side β€” identical to the sibling build by construction
The tensors are byte-identical, so the dequantization error is the same:
| Module | scheme | rel. L2 vs BF16 | β‰ˆ effective bits |
|---|---|---:|---:|
| routed experts `gate_and_up_proj` / `down_proj` | MXFP4 | **0.1120 – 0.1124** | 3.65 |
| `self_attn.*`, `shared_mlp.*` | MXFP8 | 0.0267 | 5.73 |
| router / ViT / VAE / embed (unquantized) | BF16/FP32 | 0.0000 | β€” |
MXFP4 experts carry **4.21Γ—** the relative error of the MXFP8 group (**17.7Γ—** in noise power) on
**93 %** of the parameters, and this equals the ideal per-group E8M0 search (optimized RTN) to four
decimals β€” i.e. it is the format's own floor, not a tuning miss.
### Image side (DiT-only TP2 β€” topology matched, noise floor 41.4 dB)
| Candidate | vs BF16 PSNR | SSIM@1 | @1/2 | @1/4 | **@1/8** | std |
|---|---:|---:|---:|---:|---:|---:|
| MXFP8-ct (for reference) | 31.59 | 0.979 | 0.976 | 0.978 | 0.982 | 53.62 |
| **this build (INC path)** | **12.91** | **0.6698** | 0.5417 | 0.3910 | **0.2611** | 49.34 |
| sibling `-mixed` (CT path) | 12.99 | 0.6720 | 0.5449 | 0.3963 | 0.2707 | 48.86 |
| **this build vs `-mixed`** | **33.03** | 0.9799 | 0.9772 | 0.9758 | **0.9822 (flat)** | β€” |
| BF16 base (DiT TP2) | this build (DiT TP2) | sibling `-mixed` (DiT TP2) |
|---|---|---|
| ![bf16](images/bf16_dit_only_reference.png) | ![ar](images/dit_only_tp2.png) | ![mix](images/sibling_llm_compressor_dit_only.png) |
- SSIM **falls with coarser scale** vs BF16 β‡’ **composition-level divergence**, not a texture/style
shift. The image is a valid, un-degraded *different* picture of a cute cat.
- This build vs the sibling is **flat across scales** β‡’ same composition, small texture-level numeric
differences from the different kernel entry points.
### Side-by-side montages
**Figure A β€” DiT-only, TP2 for all four builds (topology matched β‡’ the readable axis).**
β‘’ and β‘£ additionally share the *loader format* (`auto-round`/INC), so **⑒↔④ isolates the experts bit
width as the only variable.** Each cell carries its topology and PSNR / SSIM@1 / **SSIM@1/8** vs BF16
(SSIM on 1/8-scale images keeps only low-frequency structure β‡’ "same composition or not").
![DiT-only: BF16 / MXFP8-ct / MXFP8-ar / this build](images/grid_ar_ditonly.png)
| comparison | what differs | PSNR | SSIM@1 | **SSIM@1/8** |
|---|---|---:|---:|---:|
| β‘‘ ↔ β‘’ (`MXFP8-ct` vs `MXFP8-ar`) | **only the export format** | **44.46** | 0.9965 | **0.9984** |
| β‘’ ↔ β‘£ (`MXFP8-ar` vs this build) | **only experts 8β†’4 bit** | 12.71 | 0.6623 | **0.2518** |
| β‘‘ ↔ `-MXFP4-mixed` (CT side, for cross-check) | only experts 8β†’4 bit | 12.81 | 0.6643 | **0.2617** |
| β‘  ↔ β‘‘ vs BF16 | MXFP8 | 31.59 | 0.9788 | 0.9823 |
| β‘  ↔ β‘’ vs BF16 | MXFP8 (INC) | 31.79 | 0.9811 | 0.9842 |
| β‘  ↔ β‘£ vs BF16 | MXFP4 (INC) | 12.91 | 0.6698 | 0.2611 |
Noise floor: re-running the *same* build with the *same* command gives **41.4 dB / SSIM 0.995**.
β‡’ Three things worth reading off this figure:
1. **Two MXFP8 formats agree almost perfectly (44.5 dB, above the re-run floor)** β‡’ export format is
immaterial at MXFP8.
2. **Dropping the experts to MXFP4 moves SSIM@1/8 from ~0.98 to ~0.25 β€” and both independent pairings
(INC side 0.2518, CT side 0.2617) agree.** That is a *composition-level* change, not a style/texture
shift, and it is a property of MXFP4, not of either loader.
3. Unlike SSIM-vs-BF16, the ⑒↔④ pair holds topology *and* format fixed, so the 0.25 cannot be blamed on
either confounder.
**Figure B β€” AR+DiT full pipeline (only ⑒↔④ are topology-matched).**
![AR+DiT: BF16 / MXFP8-ct / MXFP8-ar / this build](images/grid_ar_ardit.png)
| comparison | topology | PSNR | SSIM@1/8 |
|---|---|---:|---:|
| β‘  ↔ β‘’ (`BF16` 4-GPU vs `MXFP8-ar` 2-GPU) | **cross-topology** | 26.85 | 0.8715 |
| β‘’ ↔ β‘£ (`MXFP8-ar` vs this build) | **both 2-GPU TP1+TP1** | 12.88 | **0.1889** |
⚠ The full pipeline **re-draws the AR CoT every run** β€” the AR text + ratio tokens are the DiT's
conditioning via `omni_kv_config` (same-build AR+DiT re-run: **27.8 dB**; and two builds with
*byte-identical* weights still disagree at SSIM@1/8 = 0.23 in this mode while agreeing at 0.98 in
DiT-only mode). Cross-topology alone already costs 0.87 β†’ 0.98. **Read Figure A, not Figure B, for
quantization fidelity; Figure B only shows that this build renders a sane image.**
### Not measured
No task-level benchmark (GenEval / DPG-Bench / CVTG-2K / DrawBench) has been run for this build or for
the `-mixed` sibling. "Different composition" is measured; "worse quality" is not established.
---
## Known limitations
1. **Requires the metadata fix** above; without it the checkpoint does not load at all (fails loudly,
which is preferable to loading silently with the wrong scheme).
2. **Memory-only on Hopper** β€” experts run W4A16 through Marlin.
3. **Composition diverges from BF16/MXFP8** (SSIM@1/8 β‰ˆ 0.26); do not assume parity, and do not mix
outputs from this build with outputs from the `-mixed` build in one A/B comparison (33 dB apart).
4. **The ViT stays BF16** (4304 % 32 β‰  0) β‡’ not a whole-model MXFP4 build.
5. The upstream INC matcher's MoE branch is keyed on a class name containing `fusedmoe`, which never
matches `RoutedExperts` on vLLM β‰₯ 0.29; this build works around the resulting brittleness by using an
explicit regex key. A cleaner fix belongs in AutoRound (emit a matchable key) or in the INC parser.
6. `block_name_to_quantize = "model.layers"` scopes quantization to the 32 shared backbone layers; AR and
DiT load the same tensors through different stacks.
7. `images/` holds verification snapshots only; it is not read when loading weights.