File size: 7,477 Bytes
4d9b003 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | # SPDX-License-Identifier: Apache-2.0
"""Precision experiment for the MLP-Mixer blocks on the device (development tool; run under ``bin/devrun``).
bin/devrun -t 1200 -- python code/scripts/precision_exp.py [--json logs/diffusion-planner/precision_exp.json]
Part A: ``ttnn.layer_norm`` vs :func:`tt.layers.layer_norm_fp32` on the real offset-dominated mixer rows (the
reference ``enc.neighbor.pre`` of the samples, block 0 ``norm1``) against float64. Part B: the 6 mixer blocks of ego /
neighbour / lane run alone on the reference's own block input (``enc.<cat>.pre``, teacher forcing) under several
option sets, compared with the reference ``enc.<cat>.mixer`` on the valid entities. Eager (no trace).
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import numpy as np
HERE = Path(__file__).resolve()
sys.path.insert(0, str(HERE.parents[1]))
from tt_diffusion_planner.reference import config as C # noqa: E402
from tt_diffusion_planner.ttaw.metrics import error_stats # noqa: E402
GOLDENS = HERE.parents[4] / "research" / "diffusion-planner" / "goldens"
CONFIGS = {
"default": dict(),
"ln_fp32": dict(ln_fp32=("enc.mixer.*",)),
"hidden_fp32": dict(hidden_fp32=("enc.mixer.*",)),
"ln_hidden_fp32": dict(ln_fp32=("enc.mixer.*",), hidden_fp32=("enc.mixer.*",)),
"all_fp32_w": dict(ln_fp32=("enc.mixer.*",), hidden_fp32=("enc.mixer.*",),
precision="enc.mixer.*=HiFi4+fp32:w=fp32:a=fp32"),
"stream_bf16": dict(precision="enc.mixer.*=HiFi4+fp32:w=bf16:a=bf16"),
}
def st(dev, ref):
s = error_stats(np.asarray(dev, np.float64), np.asarray(ref, np.float64))
return {"pcc": round(s["pcc"], 7), "rel_l2": float(f"{s['rel_l2']:.4g}"), "max_abs": float(f"{s['max_abs']:.4g}")}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--scenes", nargs="+", default=["kashiwanoha_dense", "straight_road"])
ap.add_argument("--cats", nargs="+", default=["ego", "neighbor", "lane"])
ap.add_argument("--configs", nargs="*", default=list(CONFIGS))
ap.add_argument("--json", default=None)
args = ap.parse_args()
import torch
import ttnn
torch.set_num_threads(4)
from tt_diffusion_planner.device import close_device, open_device
from tt_diffusion_planner.reference.weights import find_weights_dir, load_weights
from tt_diffusion_planner.tt.encoder import ENTITIES, MixerTrunk
from tt_diffusion_planner.tt.layers import Build, layer_norm_fp32, policy
from tt_diffusion_planner.ttaw.precision import compute_kernel_config
from tt_diffusion_planner.ttaw.tensors import to_device, to_numpy
p = load_weights(find_weights_dir()).params
gold = {}
for s in args.scenes:
with np.load(GOLDENS / f"{s}.npz", allow_pickle=False) as z:
gold[s] = {k: z[k] for k in z.files if k.startswith(("enc.", "host.valid"))}
dev = open_device(allow_fallback=False)
report = {"ln": {}, "mixer": {}}
try:
# ---- A: LayerNorm accuracy on real rows
g = p["encoder.neighbor_encoder.blocks.0.norm1.gamma"]
b = p["encoder.neighbor_encoder.blocks.0.norm1.beta"]
gd, bd = (to_device(v.reshape(1, 1, 1, -1), dev, "float32") for v in (g, b))
for s in args.scenes:
x = gold[s]["enc.neighbor.pre"].reshape(1, 1, -1, C.MIXER_CHANNELS).astype(np.float32)
xx = x.astype(np.float64)
ref = (xx - xx.mean(-1, keepdims=True)) / np.sqrt(xx.var(-1, keepdims=True) + 1e-5) * g + b
tx = to_device(x, dev, "float32")
fused = to_numpy(ttnn.layer_norm(tx, epsilon=1e-5, weight=gd, bias=bd,
compute_kernel_config=compute_kernel_config("HiFi4", fp32_acc=True)))
dec = to_numpy(layer_norm_fp32(tx, gd, bd))
report["ln"][s] = {"fused_fp32": st(fused, ref), "decomposed_fp32": st(dec, ref),
"row_offset_ratio": float(np.abs(xx.mean(-1)).mean() / xx.std(-1).mean())}
print("LN", s, json.dumps(report["ln"][s]), flush=True)
# ---- B: mixer blocks alone, teacher-forced
for name in args.configs:
cfg = dict(CONFIGS[name])
build = Build(dev, policy(spec=cfg.pop("precision", None)), **cfg)
for cat in args.cats:
trunk = MixerTrunk(build, p, cat)
for s in args.scenes:
rows = gold[s][f"enc.{cat}.pre.rows"]
if rows.size == 0:
continue
x0 = np.zeros((1, ENTITIES[cat], C.MIXER_TOKENS, C.MIXER_CHANNELS), np.float32)
x0[0, rows] = gold[s][f"enc.{cat}.pre"]
out = to_numpy(trunk.mix(to_device(x0, dev, trunk.stream)))[0, rows]
r = st(out, gold[s][f"enc.{cat}.mixer"])
report["mixer"].setdefault(name, {}).setdefault(cat, {})[s] = r
print(f"MIX {name:15s} {cat:9s} {s:18s} {json.dumps(r)}", flush=True)
del trunk
# ---- C: the island on the device (TF32-like vs split matmuls) -> pre error, and the float64 CPU mixer on
# the device pre (separates the input error from the device mixer)
from tt_diffusion_planner.host import pipeline as hp
from tt_diffusion_planner.reference.model import Encoder, torch_params
from tt_diffusion_planner.tt import inputs as I
ref_enc = Encoder(torch_params(p, torch.float64))
weights = load_weights(find_weights_dir())
def cpu_mix(x, cat):
N = f"encoder.{cat}_encoder"
x = torch.from_numpy(np.asarray(x, np.float64))
for i in range(C.MIXER_DEPTH):
B = f"{N}.blocks.{i}"
x = x + ref_enc.mlp(ref_enc.ln(x, f"{B}.norm1").transpose(1, 2), f"{B}.tokens_mlp").transpose(1, 2)
x = x + ref_enc.mlp(ref_enc.ln(x, f"{B}.norm2"), f"{B}.channels_mlp")
return x.numpy()
preps = {}
for s in args.scenes:
with np.load(GOLDENS / f"{s}.npz", allow_pickle=False) as z:
raw = {k: z[f"in.{k}"] for k in C.INPUT_NAMES}
preps[s] = I.plan_inputs(hp.prepare(raw, weights.normalization.observation))
for mode, split in (("tf32", ()), ("split", ("enc.island.*",))):
build = Build(dev, policy(), ln_fp32=("enc.mixer.*",), split=split)
for cat in ("ego", "neighbor"):
trunk = MixerTrunk(build, p, cat)
for s in args.scenes:
rows = gold[s][f"enc.{cat}.pre.rows"]
pre_t = trunk.pre(to_device(preps[s][f"{cat}_x"], dev, "float32"))
pre = to_numpy(pre_t)[0, rows]
mix = to_numpy(trunk.mix(pre_t))[0, rows]
r = {"pre": st(pre, gold[s][f"enc.{cat}.pre"]),
"mix_device": st(mix, gold[s][f"enc.{cat}.mixer"]),
"mix_cpu64_on_device_pre": st(cpu_mix(pre, cat), gold[s][f"enc.{cat}.mixer"])}
report.setdefault("island", {}).setdefault(mode, {}).setdefault(cat, {})[s] = r
print(f"ISL {mode:6s} {cat:9s} {s:18s} {json.dumps(r)}", flush=True)
del trunk
finally:
close_device(dev)
if args.json:
Path(args.json).write_text(json.dumps(report, indent=1) + "\n")
return 0
if __name__ == "__main__":
sys.exit(main())
|