[test]: calibrate bf16 encoder parity thresholds against observed Z-Image-Turbo

Per-layer bf16 diagnostic test on Z-Image-Turbo (Qwen3 35 layers, not the
24 originally assumed) confirmed the divergence between FastVideo's
Qwen3 port and HF Qwen3 is a textbook bf16-tail signature, not a code
bug:
- fp32 forward is bit-exact (last_hidden_state max=0.0000)
- bf16 per-layer mean drift grows smoothly from 5e-4 to 7.5e-2 over 35
  layers; no single-layer spike (consecutive layer ratio ≤ 1.5x)
- embedding (idx 0) is bit-exact in bf16 too — weight load is right

Root cause is well-understood: FastVideo's TP-aware fused linears
(QKVParallel, MergedColumnParallel, SiluAndMul) execute matmul ops in a
different order than HF's unfused equivalents. In fp32 the per-element
differences cancel statistically (~zero mean drift); in bf16 the
rounding-order accumulates into a long max tail.

Per add-model-02-parity calibration block: replace element-wise
assert_close (meaningless when the max tail is 4.0) with distribution
checks (mean + median). Thresholds calibrated to empirical observation:
  last_hidden_state (post-norm):  mean < 0.025, median < 0.020
  hidden_states[-2] (pre-norm):   mean < 0.120, median < 0.100
Headroom is ~1.6x over observed (0.0152, 0.0117, 0.0754, 0.0625).
fp32 path keeps strict assert_close at atol=1e-4.

Also revert `torch_dtype=dtype` -> `dtype=dtype`: transformers 4.57.3
emits a deprecation warning recommending `dtype=`. The earlier switch
(Copilot review) was wrong-direction for the pinned transformers version.
The two config-key strips that mention `torch_dtype` stay — they're
stripping the legacy serialized config key, not the constructor kwarg.

PORT_STATUS.md updated: pinned Z-Image SHA, pinned HF id (Z-Image-Turbo),
empirical bf16 signature recorded in the decisions table, 35-layer
correction reflected in the parity table.
This commit is contained in:
Raghav
2026-07-13 17:26:46 -07:00
committed by SolitaryThinker
parent 7390f528c6
commit e4eef6ed20
2 changed files with 54 additions and 30 deletions
+13 -10
View File
@@ -3,9 +3,9 @@
## Summary
- model_family: zimage
- workload_types: T2I
- official_ref: Tongyi-MAI / Z-Image (Qwen3-based T2I)
- official_ref_dir: `<repo_root>/Z-Image/src` (cloned manually; SHA not yet pinned)
- hf_weights_path: `<TODO: pin published HF id>`
- official_ref: Tongyi-MAI / Z-Image (Qwen3-based S3-DiT T2I, Apache-2.0)
- official_ref_dir: `<repo_root>/Z-Image/src` (clone `github.com/Tongyi-MAI/Z-Image`, pinned to `26f23eda626ffadda020b04ff79488e1d72004cd`)
- hf_weights_path: `Tongyi-MAI/Z-Image-Turbo` (also `Tongyi-MAI/Z-Image` for the full model — components are interchangeable for parity)
- local_weights_dir: `<repo_root>/official_weights/Z-Image/` (subfolders: `text_encoder/`, `tokenizer/`, `vae/`, `scheduler/`)
- source_layout: diffusers
- local_tests_readme: `tests/local_tests/zimage/README.md`
@@ -37,16 +37,18 @@
## Parity Commands
| Scope | Command | Last Result | Notes |
|---|---|---|---|
| Scheduler | `pytest tests/local_tests/zimage/test_zimage_scheduler_parity.py -v -s` | PASS | full `scheduler_config.json` now forwarded (was 3 keys) |
| Tokenizer | `pytest tests/local_tests/zimage/test_zimage_tokenizer_parity.py -v -s` | PASS | `apply_chat_template` parity included |
| VAE decode | `pytest tests/local_tests/zimage/test_zimage_vae_parity.py -v -s` | PASS | encode-path deferred |
| Text encoder | `pytest tests/local_tests/zimage/test_zimage_encoder_parity.py -v -s` | PASS (fp32 + bf16) | bf16 uses `atol=0.05` + abs-mean drift check + per-batch diagnostics |
| Scheduler | `pytest tests/local_tests/zimage/test_zimage_scheduler_parity.py -v -s` | PASS (2/2) on Z-Image-Turbo, validated 2026-05-12 | full `scheduler_config.json` now forwarded (was 3 keys) |
| Tokenizer | `pytest tests/local_tests/zimage/test_zimage_tokenizer_parity.py -v -s` | PASS (2/2) on Z-Image-Turbo, validated 2026-05-12 | tokenizer resolves to `Qwen2TokenizerFast`; `apply_chat_template` parity included |
| VAE decode | `pytest tests/local_tests/zimage/test_zimage_vae_parity.py -v -s` | PASS (1/1) on Z-Image-Turbo, validated 2026-05-12 | encode-path deferred |
| Text encoder fp32 | `pytest tests/local_tests/zimage/test_zimage_encoder_parity.py::test_zimage_qwen3_encoder_parity_forward[fp32]` | PASS, validated 2026-05-12 | bit-exact (`last_hidden_state` max=0.0000, `hidden_states[-2]` max=0.0012) — FastVideo Qwen3 port is numerically correct |
| Text encoder bf16 | `pytest tests/local_tests/zimage/test_zimage_encoder_parity.py::test_zimage_qwen3_encoder_parity_forward[bf16]` | PASS after threshold calibration, validated 2026-05-12 | empirical: `last_hidden_state` mean=0.0152 median=0.0117, `hidden_states[-2]` mean=0.0754 median=0.0625. Per-layer diag confirms monotonic accumulation across 35 layers, no single-layer spike — textbook bf16-tail signature |
| Per-layer bf16 diag | `pytest tests/local_tests/zimage/test_zimage_encoder_parity.py::test_zimage_qwen3_encoder_per_layer_bf16_diagnostic -v -s` | PASS (informational only), validated 2026-05-12 | prints 37 hidden-state diffs (embedding + 35 layers + post-norm) for future debugging |
## Open Questions
| ID | Question | Owner | Needed By Phase | Status | Resolution |
|---|---|---|---|---|---|
| Q001 | Pin a Z-Image reference clone SHA in the README before handoff | prep | Phase 1 | open | |
| Q002 | Final HF id for published Z-Image weights | prep | Phase 1 | open | |
| Q001 | Pin a Z-Image reference clone SHA in the README before handoff | prep | Phase 1 | resolved | Pinned `Tongyi-MAI/Z-Image@26f23eda626ffadda020b04ff79488e1d72004cd` (2026-05-12) |
| Q002 | Final HF id for published Z-Image weights | prep | Phase 1 | resolved | `Tongyi-MAI/Z-Image-Turbo` (6B, 8 NFE, fits 16 GB) and `Tongyi-MAI/Z-Image` (full, 32.9 GB). Both Apache-2.0 |
| Q003 | Does Z-Image use Qwen3 chat-template tokenization at pipeline runtime? Currently `Qwen3Config.is_chat_model=False` | pipeline | Phase 6 | open | |
## Issues And Blockers
@@ -65,7 +67,8 @@
## Decisions
| Date | Decision | Rationale | Impact |
|---|---|---|---|
| 2026-05-12 | bf16 encoder parity uses `atol=0.05` + abs-mean drift < 5e-3 + per-batch max/mean/median/p99 diagnostics. | 24-layer Qwen3 in bf16 accumulates per-GEMM epsilon into a long tail; median-near-zero + abs-mean-below-threshold catches real bugs (mean drift signature), per add-model-02-parity calibration block. | Replaces the silent fp32 downgrade. |
| 2026-05-12 | bf16 encoder parity uses distribution checks (mean + median) instead of element-wise `assert_close`; thresholds calibrated to empirical Z-Image-Turbo numbers + 1.6x headroom. | Z-Image-Turbo's Qwen3 text encoder is 35 layers (not the 24 originally assumed). Cross-kernel bf16 (FastVideo's fused QKVParallel + MergedColumnParallel + SiluAndMul vs HF's unfused equivalents) accumulates into a long max tail (~4.0 at layer 34) but median stays low (0.06). Per-layer diagnostic test confirmed growth is smooth and monotonic with no single-layer spike — textbook bf16-tail signature, fp32 is bit-exact. Element-wise `assert_close` is meaningless on this profile; mean + median + the per-layer diag together detect real bugs (which push mean ≫ atol AND median > 0.01). | Final assertion shape: `last_hidden_state` mean < 0.025, median < 0.020; `hidden_states[-2]` mean < 0.120, median < 0.100. |
| 2026-05-12 | `AutoModel.from_pretrained` uses `dtype=` (not `torch_dtype=`). | transformers 4.57.3 emits `torch_dtype is deprecated! Use dtype instead!` warning. Mrinaald's original `dtype=` kwarg was correct; the temporary switch to `torch_dtype=` (in response to a Copilot review comment) was reverted. | – |
| 2026-05-12 | Scheduler parity forwards the full `scheduler_config.json` dict (minus Diffusers loader keys), not 3 hand-picked keys. | Future on-disk fields (`time_shift_type`, `invert_sigmas`, etc.) would have been silently dropped. | Makes parity reflect the actual on-disk config. |
## Handoff Notes
@@ -77,13 +77,24 @@ def _iter_pretrained_safetensors(model_dir: Path):
)
# bf16 tolerance reflects accumulated drift across 24 transformer blocks
# (per add-model-02-parity calibration block). Diagnostic prints below assert
# the *distribution* is healthy — median≈0 and mean ≪ atol prove the bulk of
# elements match; only the tail diverges from per-GEMM bf16 epsilon.
_BF16_ATOL = 0.05
_BF16_RTOL = 0.05
_BF16_MAX_MEAN_DRIFT = 5e-3
# bf16 tolerance reflects accumulated drift across the Qwen3 encoder
# (Z-Image-Turbo ships a 35-layer Qwen3 variant). Per
# add-model-02-parity's calibration block, bf16 max-based asserts are
# meaningless for deep encoders — fused-vs-unfused linear order alone
# produces a long tail. We use distribution checks (mean + median)
# instead. The per-layer diagnostic test in this file confirmed the
# growth is monotonic with layer depth (no single-layer spike), and
# fp32 forwards are bit-exact, so this is the textbook bf16-tail
# signature and not a code bug.
#
# Empirical numbers (Z-Image-Turbo Qwen3, 35 layers, observed on CUDA):
# last_hidden_state (post-norm): mean=0.0152, median=0.0117
# hidden_states[-2] (pre-norm): mean=0.0754, median=0.0625
# Thresholds below add ~1.6x headroom over observed.
_BF16_MEAN_DRIFT_POST_NORM = 0.025
_BF16_MEAN_DRIFT_PRE_NORM = 0.120
_BF16_MEDIAN_DRIFT_POST_NORM = 0.020
_BF16_MEDIAN_DRIFT_PRE_NORM = 0.100
_FP32_ATOL = 1e-4
_FP32_RTOL = 1e-4
@@ -142,7 +153,7 @@ def test_zimage_qwen3_encoder_parity_forward(dtype: torch.dtype):
str(ZIMAGE_TEXT_ENCODER_DIR),
local_files_only=True,
trust_remote_code=True,
torch_dtype=dtype,
dtype=dtype,
low_cpu_mem_usage=True,
).eval().to(device=device, dtype=dtype)
@@ -206,8 +217,6 @@ def test_zimage_qwen3_encoder_parity_forward(dtype: torch.dtype):
# Compare parity on that exact path to avoid padded-token artifacts.
mask_cpu = attention_mask.detach().bool().cpu()
is_bf16 = dtype == torch.bfloat16
atol = _BF16_ATOL if is_bf16 else _FP32_ATOL
rtol = _BF16_RTOL if is_bf16 else _FP32_RTOL
for i in range(mask_cpu.shape[0]):
valid = mask_cpu[i]
@@ -220,19 +229,31 @@ def test_zimage_qwen3_encoder_parity_forward(dtype: torch.dtype):
hs_m2_diff = _print_diag(f"batch{i} hidden_states[-2] {dtype}", ref_hs_m2_v, fv_hs_m2_v)
if is_bf16:
# In bf16 we expect the long tail of per-GEMM epsilon to push max
# past atol; require median≈0 and abs-mean drift below threshold.
assert last_diff.mean().item() < _BF16_MAX_MEAN_DRIFT, (
# Distribution checks instead of element-wise assert_close: cross-
# kernel bf16 produces a long max tail (fused vs unfused linears),
# but a real op bug pushes mean *and* median high. Mean catches
# systematic drift; median catches "wrong weights / swapped
# layers" where most elements diverge. Pre-norm tensors get a
# looser bound because the final RMSNorm compresses drift ~5x.
assert last_diff.mean().item() < _BF16_MEAN_DRIFT_POST_NORM, (
f"batch{i} last_hidden_state bf16 mean drift "
f"{last_diff.mean():.4f} >= {_BF16_MAX_MEAN_DRIFT}"
f"{last_diff.mean():.4f} >= {_BF16_MEAN_DRIFT_POST_NORM}"
)
assert hs_m2_diff.mean().item() < _BF16_MAX_MEAN_DRIFT, (
assert last_diff.median().item() < _BF16_MEDIAN_DRIFT_POST_NORM, (
f"batch{i} last_hidden_state bf16 median drift "
f"{last_diff.median():.4f} >= {_BF16_MEDIAN_DRIFT_POST_NORM}"
)
assert hs_m2_diff.mean().item() < _BF16_MEAN_DRIFT_PRE_NORM, (
f"batch{i} hidden_states[-2] bf16 mean drift "
f"{hs_m2_diff.mean():.4f} >= {_BF16_MAX_MEAN_DRIFT}"
f"{hs_m2_diff.mean():.4f} >= {_BF16_MEAN_DRIFT_PRE_NORM}"
)
assert_close(ref_last_v, fv_last_v, atol=atol, rtol=rtol)
assert_close(ref_hs_m2_v, fv_hs_m2_v, atol=atol, rtol=rtol)
assert hs_m2_diff.median().item() < _BF16_MEDIAN_DRIFT_PRE_NORM, (
f"batch{i} hidden_states[-2] bf16 median drift "
f"{hs_m2_diff.median():.4f} >= {_BF16_MEDIAN_DRIFT_PRE_NORM}"
)
else:
assert_close(ref_last_v, fv_last_v, atol=_FP32_ATOL, rtol=_FP32_RTOL)
assert_close(ref_hs_m2_v, fv_hs_m2_v, atol=_FP32_ATOL, rtol=_FP32_RTOL)
@pytest.mark.skipif(not ZIMAGE_TEXT_ENCODER_DIR.exists(), reason="Z-Image text encoder checkpoint required")
@@ -272,7 +293,7 @@ def test_zimage_qwen3_encoder_per_layer_bf16_diagnostic():
str(ZIMAGE_TEXT_ENCODER_DIR),
local_files_only=True,
trust_remote_code=True,
torch_dtype=dtype,
dtype=dtype,
low_cpu_mem_usage=True,
).eval().to(device=device, dtype=dtype)