631 lines
24 KiB
Python
631 lines
24 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Per-layer fp8 ablation probe for video DiTs (Wan / Hunyuan): find which layers -- if any --
|
|
break under production per-row fp8 (torch._scaled_mm), or prove the failure is systemic.
|
|
|
|
Motivation: on Blackwell the auto ladder leads with fp8, but on Wan2.2 and HunyuanVideo-1.5 that
|
|
renders every frame BLACK (int8 is clean), so the shipped fix denies fp8 -> int8. Is the black
|
|
frame a small nameable set of outlier layers (keep fp8 on the rest), or systemic to _scaled_mm
|
|
(so per-layer exclusion can't help and int8 stays right)?
|
|
|
|
Method (single-forward proxy): run one bf16 generation, capture a REAL mid-schedule DiT input
|
|
via a forward_pre_hook, then compare the dense output to the output after quantising layer
|
|
SUBSETS with the production fp8 config. Reuses ``_make_quant_config`` / ``make_filter_fn``, so
|
|
the GEMM path is byte-identical to production.
|
|
|
|
Phase 0 (default) is the gate:
|
|
0a proxy-validity : fp8-ALL must reproduce the black signal in one forward. If it's CLEAN, the
|
|
black frame is multi-step / cache compounding -> single-forward bisection
|
|
can't localise it -> STOP, keep int8.
|
|
0b mechanism : (i) per-Linear finiteness hooks locate the first inf/NaN; (ii) flip
|
|
use_fast_accum -- if it flips black->clean the fix is a one-line accumulate
|
|
flag.
|
|
Also records per-Linear input outlier stats on the bf16 forward for the Phase 2 ranking.
|
|
|
|
Example:
|
|
CUDA_VISIBLE_DEVICES=1 python scripts/fp8_layer_ablation.py --family wan2.2-ti2v-5b --phase 0
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import copy
|
|
import gc
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
os.environ.setdefault("BITSANDBYTES_NOWELCOME", "1")
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
_BACKEND_ROOT = _REPO_ROOT / "studio" / "backend"
|
|
for _p in (str(_BACKEND_ROOT), str(_REPO_ROOT / "scripts")):
|
|
if _p not in sys.path:
|
|
sys.path.insert(0, _p)
|
|
|
|
# Reuse the benchmark's real pipeline-build + family plumbing so the load is identical.
|
|
from video_speedmem_bench import _FAMILIES, PROMPT, _build_pipe, _import_diffusers # noqa: E402
|
|
from core.inference.diffusion_transformer_quant import ( # noqa: E402
|
|
DEFAULT_MIN_LINEAR_FEATURES,
|
|
TQ_FP8,
|
|
_REQUIRE_BF16_SCHEMES,
|
|
_make_quant_config,
|
|
make_filter_fn,
|
|
)
|
|
|
|
|
|
# ── cuda helpers ────────────────────────────────────────────────────────────────
|
|
def _empty() -> None:
|
|
import torch
|
|
gc.collect()
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
|
|
|
|
def _detach_to_cpu(obj: Any) -> Any:
|
|
"""Recursively detach + move tensors in a nested args/kwargs structure to CPU."""
|
|
import torch
|
|
|
|
if torch.is_tensor(obj):
|
|
return obj.detach().to("cpu")
|
|
if isinstance(obj, tuple):
|
|
return tuple(_detach_to_cpu(o) for o in obj)
|
|
if isinstance(obj, list):
|
|
return [_detach_to_cpu(o) for o in obj]
|
|
if isinstance(obj, dict):
|
|
return {k: _detach_to_cpu(v) for k, v in obj.items()}
|
|
return obj
|
|
|
|
|
|
def _to_device(obj: Any, device: str) -> Any:
|
|
"""Recursively move tensors to ``device`` (dtypes preserved -- timestep stays int)."""
|
|
import torch
|
|
|
|
if torch.is_tensor(obj):
|
|
return obj.to(device)
|
|
if isinstance(obj, tuple):
|
|
return tuple(_to_device(o, device) for o in obj)
|
|
if isinstance(obj, list):
|
|
return [_to_device(o, device) for o in obj]
|
|
if isinstance(obj, dict):
|
|
return {k: _to_device(v, device) for k, v in obj.items()}
|
|
return obj
|
|
|
|
|
|
def _extract_tensor(out: Any):
|
|
import torch
|
|
|
|
if torch.is_tensor(out):
|
|
return out
|
|
s = getattr(out, "sample", None)
|
|
if torch.is_tensor(s):
|
|
return s
|
|
if isinstance(out, (list, tuple)) and out and torch.is_tensor(out[0]):
|
|
return out[0]
|
|
return out
|
|
|
|
|
|
# ── forward-tuple capture ────────────────────────────────────────────────────────
|
|
class _Stop(Exception):
|
|
pass
|
|
|
|
|
|
def _capture_forward_tuple(
|
|
pipe, *, steps, width, height, num_frames, guidance, seed, capture_call, gvg
|
|
):
|
|
"""Run a bf16 generation and grab the ``capture_call``-th transformer forward's
|
|
(args, kwargs) to CPU, then abort. This is a REAL mid-schedule DiT input."""
|
|
import torch
|
|
|
|
holder: dict[str, Any] = {}
|
|
calls = [0]
|
|
|
|
def _pre(mod, args, kwargs):
|
|
calls[0] += 1
|
|
if calls[0] == capture_call:
|
|
holder["args"] = _detach_to_cpu(args)
|
|
holder["kwargs"] = _detach_to_cpu(kwargs)
|
|
raise _Stop()
|
|
return None
|
|
|
|
handle = pipe.transformer.register_forward_pre_hook(_pre, with_kwargs = True)
|
|
g = torch.Generator(device = "cuda").manual_seed(seed)
|
|
kwargs = dict(
|
|
prompt = PROMPT,
|
|
width = width,
|
|
height = height,
|
|
num_frames = num_frames,
|
|
num_inference_steps = steps,
|
|
generator = g,
|
|
)
|
|
if gvg:
|
|
guider = getattr(pipe, "guider", None)
|
|
if guider is not None and hasattr(guider, "guidance_scale"):
|
|
try:
|
|
guider.guidance_scale = guidance
|
|
except Exception:
|
|
pass
|
|
else:
|
|
kwargs["guidance_scale"] = guidance
|
|
try:
|
|
pipe(**kwargs)
|
|
except _Stop:
|
|
pass
|
|
finally:
|
|
handle.remove()
|
|
if "kwargs" not in holder:
|
|
raise RuntimeError(
|
|
f"transformer forward never reached call #{capture_call} "
|
|
f"(saw {calls[0]}); lower --capture-call"
|
|
)
|
|
return holder
|
|
|
|
|
|
def _forward_out(module, tup: dict, device: str):
|
|
import torch
|
|
|
|
args = _to_device(tup.get("args", ()), device)
|
|
kwargs = _to_device(tup.get("kwargs", {}), device)
|
|
with torch.no_grad():
|
|
out = module(*args, **kwargs)
|
|
return _extract_tensor(out)
|
|
|
|
|
|
# ── scoring ──────────────────────────────────────────────────────────────────────
|
|
def _score(ref, cand) -> dict:
|
|
"""Compare a candidate DiT output to the bf16 reference. Non-finite entries are the
|
|
strongest black-frame signal; cosine / relL2 / norm-ratio are computed on finite entries."""
|
|
import torch
|
|
|
|
reff = ref.detach().flatten().float()
|
|
canf = cand.detach().flatten().float()
|
|
fin_c = torch.isfinite(canf)
|
|
frac_nonfinite = float(1.0 - fin_c.float().mean().item())
|
|
mask = fin_c & torch.isfinite(reff)
|
|
if mask.any():
|
|
a = canf[mask]
|
|
b = reff[mask]
|
|
cos = float(torch.nn.functional.cosine_similarity(a.unsqueeze(0), b.unsqueeze(0)).item())
|
|
rel = float(
|
|
(torch.linalg.vector_norm(a - b) / (torch.linalg.vector_norm(b) + 1e-12)).item()
|
|
)
|
|
norm_ratio = float(
|
|
(torch.linalg.vector_norm(a) / (torch.linalg.vector_norm(b) + 1e-12)).item()
|
|
)
|
|
else:
|
|
cos, rel, norm_ratio = 0.0, float("inf"), 0.0
|
|
return {
|
|
"cosine": round(cos, 5),
|
|
"relL2": round(rel, 5),
|
|
"frac_nonfinite": round(frac_nonfinite, 6),
|
|
"norm_ratio": round(norm_ratio, 5),
|
|
"cand_amax": round(
|
|
float(canf[fin_c].abs().max().item()) if fin_c.any() else float("inf"), 3
|
|
),
|
|
}
|
|
|
|
|
|
def _verdict(sc: dict) -> str:
|
|
"""Black-frame proxy verdict from a score dict."""
|
|
if sc["frac_nonfinite"] > 0:
|
|
return "BROKEN(non-finite)"
|
|
if sc["cosine"] < 0.5 or sc["norm_ratio"] < 0.3 or sc["norm_ratio"] > 3.0:
|
|
return "BROKEN(collapse)"
|
|
if sc["cosine"] < 0.98:
|
|
return "DEGRADED"
|
|
return "CLEAN"
|
|
|
|
|
|
# ── quant helpers ──────────────────────────────────────────────────────────────
|
|
def _fp8_filter(
|
|
min_features: int,
|
|
exclude_tokens = (),
|
|
only_tokens = (),
|
|
):
|
|
"""A production fp8 filter (require_bf16) optionally narrowed:
|
|
- exclude_tokens: skip linears whose fqn contains any token (keep them bf16)
|
|
- only_tokens : quantise ONLY linears whose fqn contains any token."""
|
|
base = make_filter_fn(
|
|
min_features,
|
|
exclude_name_tokens = exclude_tokens,
|
|
require_bf16 = (TQ_FP8 in _REQUIRE_BF16_SCHEMES),
|
|
)
|
|
|
|
def filt(module, fqn: str = "") -> bool:
|
|
if not base(module, fqn):
|
|
return False
|
|
name = (fqn or "").lower()
|
|
if exclude_tokens and any(t in name for t in exclude_tokens):
|
|
return False
|
|
if only_tokens and not any(t in name for t in only_tokens):
|
|
return False
|
|
return True
|
|
|
|
return filt
|
|
|
|
|
|
def _ablate(
|
|
dense_cpu,
|
|
tup,
|
|
*,
|
|
min_features,
|
|
fast_accum = None,
|
|
exclude_tokens = (),
|
|
only_tokens = (),
|
|
instrument = False,
|
|
):
|
|
"""Deepcopy the dense DiT -> GPU -> fp8-quantise the selected subset -> one forward.
|
|
Returns (cpu_output_tensor, finiteness_records|None)."""
|
|
import torch
|
|
from torchao.quantization import quantize_
|
|
|
|
m = copy.deepcopy(dense_cpu).to("cuda")
|
|
filt = _fp8_filter(min_features, exclude_tokens = exclude_tokens, only_tokens = only_tokens)
|
|
quantize_(m, _make_quant_config(TQ_FP8, fast_accum = fast_accum), filter_fn = filt)
|
|
|
|
records = None
|
|
hooks = []
|
|
if instrument:
|
|
records = []
|
|
|
|
def mk(nm):
|
|
def hook(mod, inp, out):
|
|
o = (
|
|
out
|
|
if torch.is_tensor(out)
|
|
else (out[0] if isinstance(out, (list, tuple)) and out else None)
|
|
)
|
|
if o is None:
|
|
return
|
|
of = o.detach().float()
|
|
in_amax = None
|
|
in_frac_zero_rows = None
|
|
if inp and torch.is_tensor(inp[0]):
|
|
xin = inp[0].detach().float()
|
|
in_amax = float(xin.abs().max().item())
|
|
if xin.dim() >= 2:
|
|
ra = xin.reshape(-1, xin.shape[-1]).abs().amax(dim = -1)
|
|
in_frac_zero_rows = round(float((ra < 1e-6).float().mean().item()), 4)
|
|
records.append(
|
|
{
|
|
"name": nm,
|
|
"out_finite": bool(torch.isfinite(of).all().item()),
|
|
"out_amax": float(of.abs().max().item())
|
|
if torch.isfinite(of).any()
|
|
else float("inf"),
|
|
"in_amax": in_amax,
|
|
"in_frac_zero_rows": in_frac_zero_rows,
|
|
}
|
|
)
|
|
|
|
return hook
|
|
|
|
for nm, mod in m.named_modules():
|
|
if isinstance(mod, torch.nn.Linear):
|
|
hooks.append(mod.register_forward_hook(mk(nm)))
|
|
|
|
out = _forward_out(m, tup, "cuda")
|
|
out = out.detach().to("cpu")
|
|
for h in hooks:
|
|
h.remove()
|
|
del m
|
|
_empty()
|
|
return out, records
|
|
|
|
|
|
# ── bf16 reference + input outlier stats (for Phase 2 ranking) ───────────────────
|
|
# ── Phase 1 buckets (fqn substring tokens), keyed by DiT kind ────────────────────
|
|
# Grounded in Phase 0: on Wan the inf originates in layers whose ROW dimension is the padded
|
|
# text sequence (condition_embedder.text_embedder + cross-attn K/V); the video attn1 + attn2.to_q
|
|
# + FFN stay finite. "textpath" is the hypothesised minimal exclude set.
|
|
_BUCKETS: dict[str, dict[str, tuple[str, ...]]] = {
|
|
"wan": {
|
|
"textpath": ("text_embedder", "attn2.to_k", "attn2.to_v", "attn2.add_k", "attn2.add_v"),
|
|
"condition_embedder": ("condition_embedder",),
|
|
"attn2_all": ("attn2.",),
|
|
"attn2_kv": ("attn2.to_k", "attn2.to_v", "attn2.add_k", "attn2.add_v"),
|
|
"attn1": ("attn1.",),
|
|
"ffn": ("ffn.",),
|
|
},
|
|
"hunyuan": {
|
|
# candidate fix: the input embedders consuming zero-padded conditioning (all-zero T2V
|
|
# image_embeds, all-zero ByT5). "context_embedder" also matches "context_embedder_2".
|
|
"embedders": ("context_embedder", "image_embedder"),
|
|
"context_embedder": ("context_embedder",), # text refiner + context_embedder_2
|
|
"image_embedder": ("image_embedder",),
|
|
"context_embedder_2": ("context_embedder_2",),
|
|
"proj_out": ("proj_out",),
|
|
"main_blocks": ("transformer_blocks.",), # ONLY main blocks -> should be CLEAN
|
|
},
|
|
}
|
|
|
|
|
|
def _dit_kind(family: str) -> str:
|
|
return "hunyuan" if "hunyuan" in family.lower() else "wan"
|
|
|
|
|
|
def _zero_row_diag(tup: dict) -> dict:
|
|
"""Test the divide-by-zero hypothesis: fraction of near-zero (padding) rows. A per-row fp8
|
|
scale = row_amax / 448, so row_amax 0 -> scale 0 -> x/0 = inf. Padded text is the suspect."""
|
|
import torch
|
|
|
|
diag = {}
|
|
for key, val in tup.get("kwargs", {}).items():
|
|
if torch.is_tensor(val) and val.dim() >= 2 and val.is_floating_point():
|
|
x = val.float().reshape(-1, val.shape[-1])
|
|
row_amax = x.abs().amax(dim = -1)
|
|
n = int(row_amax.numel())
|
|
n_zero = int((row_amax < 1e-6).sum().item())
|
|
diag[key] = {
|
|
"rows": n,
|
|
"zero_amax_rows": n_zero,
|
|
"frac_zero_rows": round(n_zero / n, 4) if n else 0.0,
|
|
"min_row_amax": round(float(row_amax.min().item()), 8),
|
|
}
|
|
return diag
|
|
|
|
|
|
def _bf16_reference_and_stats(dense_gpu, tup, *, min_features):
|
|
"""Reference output + per-Linear input outlier stats on the bf16 forward. ``spread`` = mean
|
|
over tokens of (channel amax / channel median-abs). Only fp8-quantised linears are recorded."""
|
|
import torch
|
|
|
|
keep = _fp8_filter(min_features) # which linears fp8 would touch
|
|
stats: list[dict] = []
|
|
hooks = []
|
|
|
|
def mk(nm):
|
|
def hook(mod, inp, out):
|
|
if not inp or not torch.is_tensor(inp[0]):
|
|
return
|
|
x = inp[0].detach().float()
|
|
x2 = x.reshape(-1, x.shape[-1]).abs() # [tokens, channels]
|
|
amax_tok = x2.amax(dim = -1)
|
|
med_tok = x2.median(dim = -1).values
|
|
spread = float((amax_tok / (med_tok + 1e-9)).mean().item())
|
|
stats.append(
|
|
{
|
|
"name": nm,
|
|
"in_amax": float(x2.max().item()),
|
|
"spread": round(spread, 2),
|
|
"in_features": int(mod.in_features),
|
|
"out_features": int(mod.out_features),
|
|
}
|
|
)
|
|
|
|
return hook
|
|
|
|
for nm, mod in dense_gpu.named_modules():
|
|
if isinstance(mod, torch.nn.Linear) and keep(mod, nm):
|
|
hooks.append(mod.register_forward_hook(mk(nm)))
|
|
|
|
ref = _forward_out(dense_gpu, tup, "cuda")
|
|
ref = ref.detach().to("cpu")
|
|
for h in hooks:
|
|
h.remove()
|
|
stats.sort(key = lambda s: s["spread"], reverse = True)
|
|
return ref, stats
|
|
|
|
|
|
def main(argv = None) -> int:
|
|
ap = argparse.ArgumentParser(description = __doc__)
|
|
ap.add_argument("--family", default = "wan2.2-ti2v-5b", choices = sorted(_FAMILIES))
|
|
ap.add_argument("--phase", type = int, default = 0, choices = [0, 1, 2])
|
|
ap.add_argument("--only", default = "", help = "phase 2: comma tokens -- fp8 ONLY these linears")
|
|
ap.add_argument("--exclude", default = "", help = "phase 2: comma tokens -- fp8 all EXCEPT these")
|
|
ap.add_argument("--steps", type = int, default = 20)
|
|
ap.add_argument("--num-frames", type = int, default = 25)
|
|
ap.add_argument("--width", type = int, default = 512)
|
|
ap.add_argument("--height", type = int, default = 320)
|
|
ap.add_argument("--seed", type = int, default = 42)
|
|
ap.add_argument(
|
|
"--capture-call",
|
|
type = int,
|
|
default = 8,
|
|
help = "which transformer forward call to capture (~mid schedule)",
|
|
)
|
|
ap.add_argument("--min-features", type = int, default = DEFAULT_MIN_LINEAR_FEATURES)
|
|
ap.add_argument("--out", default = "outputs/fp8_ablation")
|
|
args = ap.parse_args(argv)
|
|
|
|
import torch
|
|
|
|
out = Path(args.out)
|
|
out.mkdir(parents = True, exist_ok = True)
|
|
|
|
spec = _FAMILIES[args.family]
|
|
repo = spec["repo"]
|
|
force_fp32 = spec.get("vae_force_fp32", False)
|
|
guidance = spec.get("guidance", 5.0)
|
|
|
|
from core.inference.video_families import detect_video_family
|
|
|
|
fam_obj = detect_video_family(repo)
|
|
gvg = bool(getattr(fam_obj, "guidance_via_guider", False))
|
|
|
|
print(
|
|
f"== fp8 layer ablation: family={args.family} repo={repo} phase={args.phase} ==", flush = True
|
|
)
|
|
t0 = time.perf_counter()
|
|
pipe = _build_pipe(repo, force_fp32)
|
|
# _build_pipe returns a CPU pipeline; the ablation captures a forward with a CUDA generator,
|
|
# so place it on CUDA here to avoid a cpu/cuda mismatch.
|
|
pipe = pipe.to("cuda")
|
|
print(f"[load] pipe built in {time.perf_counter()-t0:.1f}s", flush = True)
|
|
|
|
tup = _capture_forward_tuple(
|
|
pipe,
|
|
steps = args.steps,
|
|
width = args.width,
|
|
height = args.height,
|
|
num_frames = args.num_frames,
|
|
guidance = guidance,
|
|
seed = args.seed,
|
|
capture_call = args.capture_call,
|
|
gvg = gvg,
|
|
)
|
|
ks = {
|
|
k: (tuple(v.shape) if torch.is_tensor(v) else type(v).__name__)
|
|
for k, v in tup.get("kwargs", {}).items()
|
|
}
|
|
print(f"[capture] call #{args.capture_call} kwargs={ks}", flush = True)
|
|
|
|
# Keep the dense bf16 DiT (still on GPU) as reference source; copy to CPU as the ablation seed.
|
|
dense_gpu = pipe.transformer
|
|
ref, stats = _bf16_reference_and_stats(dense_gpu, tup, min_features = args.min_features)
|
|
print(
|
|
f"[bf16 ref] out shape={tuple(ref.shape)} amax={float(ref.float().abs().max()):.3f} "
|
|
f"n_quantized_linears={len(stats)}",
|
|
flush = True,
|
|
)
|
|
|
|
dense_cpu = copy.deepcopy(dense_gpu).to("cpu")
|
|
# Free the pipeline (VAE/text encoders) to leave the GPU for one transformer at a time.
|
|
del pipe, dense_gpu
|
|
_empty()
|
|
|
|
report: dict[str, Any] = {
|
|
"family": args.family,
|
|
"repo": repo,
|
|
"capture_call": args.capture_call,
|
|
"input_shapes": ks,
|
|
"ref_amax": round(float(ref.float().abs().max()), 3),
|
|
"zero_row_diag": _zero_row_diag(tup),
|
|
}
|
|
print(f"[zero-row diag] {report['zero_row_diag']}", flush = True)
|
|
|
|
if args.phase == 0:
|
|
# ── Phase 0a: proxy validity -- fp8-ALL (production accum = None auto-detect) ──
|
|
out_all, _ = _ablate(dense_cpu, tup, min_features = args.min_features, fast_accum = None)
|
|
sc_all = _score(ref, out_all)
|
|
report["fp8_all_autoaccum"] = {**sc_all, "verdict": _verdict(sc_all)}
|
|
print(f"[0a fp8-ALL auto-accum] {sc_all} -> {_verdict(sc_all)}", flush = True)
|
|
|
|
# ── Phase 0b-ii: accumulate flip ──
|
|
for fa in (False, True):
|
|
o, _ = _ablate(dense_cpu, tup, min_features = args.min_features, fast_accum = fa)
|
|
sc = _score(ref, o)
|
|
report[f"fp8_all_fast_accum_{fa}"] = {**sc, "verdict": _verdict(sc)}
|
|
print(f"[0b accum={fa}] {sc} -> {_verdict(sc)}", flush = True)
|
|
|
|
# ── Phase 0b-i: per-Linear finiteness (instrumented fp8-ALL forward) ──
|
|
_, records = _ablate(
|
|
dense_cpu, tup, min_features = args.min_features, fast_accum = None, instrument = True
|
|
)
|
|
if records:
|
|
nonfinite = [r for r in records if not r["out_finite"]]
|
|
report["first_nonfinite_linears"] = nonfinite[:10]
|
|
report["n_nonfinite_linears"] = len(nonfinite)
|
|
finite_sorted = sorted(
|
|
[r for r in records if r["out_finite"]], key = lambda r: r["out_amax"], reverse = True
|
|
)
|
|
report["top_out_amax_linears"] = finite_sorted[:10]
|
|
print(
|
|
f"[0b finiteness] {len(nonfinite)}/{len(records)} linears non-finite; "
|
|
f"first={[r['name'] for r in nonfinite[:5]]}",
|
|
flush = True,
|
|
)
|
|
print(
|
|
f"[0b top out_amax] {[(r['name'], round(r['out_amax'],1)) for r in finite_sorted[:5]]}",
|
|
flush = True,
|
|
)
|
|
|
|
report["top_spread_linears"] = stats[:20]
|
|
print(
|
|
f"[stats] top-spread linears: " f"{[(s['name'], s['spread']) for s in stats[:8]]}",
|
|
flush = True,
|
|
)
|
|
|
|
va = report["fp8_all_autoaccum"]["verdict"]
|
|
if va.startswith("BROKEN"):
|
|
print(
|
|
"\n[GATE] fp8-ALL reproduces the black signal in one forward -> proxy VALID; "
|
|
"proceed to Phase 1 bucket ablation.",
|
|
flush = True,
|
|
)
|
|
elif va == "CLEAN":
|
|
print(
|
|
"\n[GATE] fp8-ALL is CLEAN in one forward but full gen is black -> failure is "
|
|
"multi-step/cache; single-forward bisection cannot localise -> STOP, keep int8.",
|
|
flush = True,
|
|
)
|
|
else:
|
|
print(f"\n[GATE] fp8-ALL verdict={va}: borderline; inspect scores.", flush = True)
|
|
|
|
elif args.phase == 1: # ── Phase 1: bucket ablation (necessity + sufficiency) ──
|
|
kind = _dit_kind(args.family)
|
|
buckets = _BUCKETS[kind]
|
|
results: dict[str, Any] = {}
|
|
|
|
# baseline: fp8-ALL (should be BROKEN, matching Phase 0)
|
|
o, _ = _ablate(dense_cpu, tup, min_features = args.min_features)
|
|
results["fp8_all"] = {**_score(ref, o)}
|
|
results["fp8_all"]["verdict"] = _verdict(results["fp8_all"])
|
|
print(
|
|
f"[1 baseline fp8-ALL] {results['fp8_all']['verdict']} {results['fp8_all']}", flush = True
|
|
)
|
|
|
|
for bname, toks in buckets.items():
|
|
# NECESSITY: fp8 everything EXCEPT this bucket -- if CLEAN, this bucket is the culprit.
|
|
o, _ = _ablate(dense_cpu, tup, min_features = args.min_features, exclude_tokens = toks)
|
|
sc_ex = _score(ref, o)
|
|
# SUFFICIENCY: fp8 ONLY this bucket -- if BROKEN, this bucket alone reproduces damage.
|
|
o2, _ = _ablate(dense_cpu, tup, min_features = args.min_features, only_tokens = toks)
|
|
sc_only = _score(ref, o2)
|
|
results[bname] = {
|
|
"exclude": {**sc_ex, "verdict": _verdict(sc_ex)},
|
|
"only": {**sc_only, "verdict": _verdict(sc_only)},
|
|
}
|
|
print(
|
|
f"[1 {bname}] EXCLUDE->{_verdict(sc_ex)} (cos {sc_ex['cosine']}, "
|
|
f"nf {sc_ex['frac_nonfinite']}) | ONLY->{_verdict(sc_only)} "
|
|
f"(cos {sc_only['cosine']}, nf {sc_only['frac_nonfinite']})",
|
|
flush = True,
|
|
)
|
|
|
|
report["phase1"] = results
|
|
|
|
else: # ── Phase 2: instrument a specific only/exclude set, find first overflow ──
|
|
only = tuple(t.strip() for t in args.only.split(",") if t.strip())
|
|
exclude = tuple(t.strip() for t in args.exclude.split(",") if t.strip())
|
|
o, records = _ablate(
|
|
dense_cpu,
|
|
tup,
|
|
min_features = args.min_features,
|
|
only_tokens = only,
|
|
exclude_tokens = exclude,
|
|
instrument = True,
|
|
)
|
|
sc = _score(ref, o)
|
|
report["phase2"] = {"only": only, "exclude": exclude, **sc, "verdict": _verdict(sc)}
|
|
print(f"[2 only={only} exclude={exclude}] {sc} -> {_verdict(sc)}", flush = True)
|
|
if records:
|
|
# first non-finite linears in execution order: an in_amax ~0 at the first-broken
|
|
# layer confirms a zero-amax padding row -> scale 0 -> inf.
|
|
nonfinite = [r for r in records if not r["out_finite"]]
|
|
report["phase2_first_nonfinite"] = nonfinite[:15]
|
|
report["phase2_n_nonfinite"] = len(nonfinite)
|
|
print(
|
|
f"[2 finiteness] {len(nonfinite)}/{len(records)} quantised-path linears non-finite",
|
|
flush = True,
|
|
)
|
|
for r in nonfinite[:8]:
|
|
print(
|
|
f" first-inf {r['name']} in_amax={r['in_amax']} "
|
|
f"in_frac_zero_rows={r['in_frac_zero_rows']} out_amax={r['out_amax']}",
|
|
flush = True,
|
|
)
|
|
|
|
dest = out / f"phase{args.phase}_{args.family}.json"
|
|
with open(dest, "w", encoding = "utf-8") as fh:
|
|
json.dump(report, fh, indent = 2)
|
|
print(f"wrote {dest}", flush = True)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|