frnet-p150 / code /scripts /diag_conv_precision.py
changh95's picture
tt-model push frnet-p150 (container)
fe51a63 verified
Raw History Blame Contribute Delete
9.96 kB
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Device diagnostic (M1b follow-up): where does the device lose accuracy outside the point island?
cd bundles/frnet-p150 && bin/devrun -t 1200 -- env PYTHONPATH=code python code/scripts/diag_conv_precision.py \\
--out /home/ubuntu/experiments/tt-models/logs/frnet/m1b/diag_conv_precision.json
Teacher-forced units on the OT128 sample's golden taps (torch fp32 reference): each unit's device output is compared
with the golden output (``dev``) and with a CPU emulation of the device's rounding points with exact fp32 weights
(``emu``: bf16 inputs, conv outputs rounded to bf16 before / after each activation, bf16 adds); ``dev_vs_emu``
rel-L2 measures the device's arithmetic error beyond the emulated bf16 roundings (0 for a perfect fp32 conv engine).
Units: the stem (3 convs + HardSwish), every stage's BasicBlocks (stage input -> ``layer<l>``), every attention
(``pixel_fusion<l>`` -> ``attention<l>``), the ``addcmul`` (-> ``stage<l>``), ``fuse2``, the softmax, the first conv
of each stage alone (no activation), and the element-wise ops (HardSwish, sigmoid) on bf16 inputs. Configurations:
``FRNET_CONV_TERMS`` 1 (float32 conv weights: the device's TF32-like path) and 2 (C17 two-term bf16 weights).
Eager runs (no trace): a diagnostic, not a gate.
"""
from __future__ import annotations
import argparse
import json
import sys
import time
from pathlib import Path
import numpy as np
HERE = Path(__file__).resolve()
sys.path.insert(0, str(HERE.parents[1]))
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--out", type=Path, required=True)
ap.add_argument("--terms", nargs="*", type=int, default=[1, 2])
args = ap.parse_args()
import torch
import torch.nn.functional as F
torch.set_num_threads(4)
from tt_frnet.device import device_session
from tt_frnet.reference.config import FRNetConfig
from tt_frnet.reference.weights import load_weights
from tt_frnet.tests._data import golden_dir, weights_dir
from tt_frnet.tt.layers import free
from tt_frnet.tt.model import KNOBS, LOGIT_PAD, TtFRNet
from tt_frnet.ttaw.metrics import error_stats, pcc
from tt_frnet.ttaw.ops.conv import FeatureMap, fmap_from_numpy, fmap_to_numpy
from tt_frnet.ttaw.tensors import round_to_bf16
variant = "ot128"
wdir = weights_dir()
cfg = FRNetConfig.from_model_dir(wdir, variant)
weights = load_weights(wdir, variant)
taps = np.load(golden_dir() / variant / "pandaset_019_40.taps.npz", mmap_mode="r")
def g(name):
return np.asarray(taps[name], np.float32)
def bf(a):
return round_to_bf16(np.asarray(a, np.float32))
def t(a):
return torch.from_numpy(np.ascontiguousarray(a, np.float32))
# ---- CPU emulation of the device rounding points (exact fp32 weights)
def emu_conv(x, layer, act=True):
y = F.conv2d(t(x), t(layer.weight), t(layer.bias), stride=layer.stride, padding=layer.padding).numpy()
y = bf(y)
if act and layer.activation == "hardswish":
y = bf(F.hardswish(t(y)).numpy())
elif act and layer.activation == "sigmoid":
y = bf(torch.sigmoid(t(y)).numpy())
return y
def emu_blocks(x, st):
for c1, c2, ds in st.blocks:
y = emu_conv(emu_conv(x, c1), c2)
idt = emu_conv(x, ds) if ds is not None else x
x = bf(F.hardswish(t(bf(y + idt))).numpy())
return x
def stats(dev, emu, gold):
d = error_stats(dev, gold)
e = error_stats(emu, gold)
de = error_stats(dev, emu)
return {"pcc_dev": d["pcc"], "pcc_emu": e["pcc"], "rel_l2_dev": d["rel_l2"], "rel_l2_emu": e["rel_l2"],
"rel_l2_dev_vs_emu": de["rel_l2"], "max_abs_dev_vs_emu": de["max_abs"]}
results = {"variant": variant, "frame": "pandaset_019_40", "configs": {}}
stage_inputs = ["backbone.fusion_stem", "backbone.stage1", "backbone.stage2", "backbone.stage3"]
emu_cache = {}
with device_session() as device:
for terms in args.terms:
t0 = time.time()
knobs = KNOBS.read(CONV_TERMS=terms)
net = TtFRNet(device, weights, cfg, knobs=knobs)
res = {}
def up(nchw):
return fmap_from_numpy(bf(nchw), device, dtype="bfloat16")
def report(name, dev, emu, gold):
if callable(emu): # the CPU emulation is the same for every configuration
if name not in emu_cache:
emu_cache[name] = emu()
emu = emu_cache[name]
res[name] = stats(dev, emu, gold)
r = res[name]
print(f"[terms={terms}] {name:28s} pcc dev {r['pcc_dev']:.6f} emu {r['pcc_emu']:.6f} | rel_l2 dev "
f"{r['rel_l2_dev']:.3e} emu {r['rel_l2_emu']:.3e} dev-vs-emu {r['rel_l2_dev_vs_emu']:.3e}",
flush=True)
# stem
fr = g("backbone.frustum")
fr32 = np.concatenate([fr, np.zeros_like(fr)], 1)
x = up(fr32)
for conv in net.stem:
y = conv(x)
free(x)
x = y
dev = fmap_to_numpy(x)
free(x)
def emu_stem():
e = bf(fr)
for c in weights.stem:
e = emu_conv(e, c)
return e
report("stem", dev, emu_stem, g("backbone.stem"))
# stages
for st_w, st, src in zip(weights.stages, net.stages, stage_inputs):
l = st["index"]
xin = g(src)
# first conv alone (pre-activation)
c1 = st["blocks"][0][0]
x = up(xin)
y = c1.conv(x)
dev = fmap_to_numpy(y)
free(y)
lay = st_w.blocks[0][0]
exact = F.conv2d(t(bf(xin)), t(lay.weight), t(lay.bias), stride=lay.stride,
padding=lay.padding).numpy()
report(f"layer{l}.block0.conv1(noact)", dev, bf(exact), exact)
# blocks
for c1_, c2_, ds_ in st["blocks"]:
y1 = c1_(x)
y = c2_(y1)
free(y1)
idt = ds_(x) if ds_ is not None else x
import ttnn
s = ttnn.add(y.tensor, idt.tensor, memory_config=ttnn.DRAM_MEMORY_CONFIG)
out = y.with_tensor(ttnn.hardswish(s))
free(s, y)
if ds_ is not None:
free(idt)
free(x)
x = out
dev = fmap_to_numpy(x)
free(x)
report(f"layer{l}", dev, lambda: emu_blocks(bf(xin), st_w), g(f"backbone.layer{l}"))
# attention
pfz = g(f"backbone.pixel_fusion{l}")
x = up(pfz)
a0 = st["attention"][0](x)
a1 = st["attention"][1](a0)
free(a0)
dev = fmap_to_numpy(a1)
free(a1)
report(f"attention{l}", dev,
lambda: emu_conv(emu_conv(bf(pfz), st_w.attention[0]), st_w.attention[1]),
g(f"backbone.attention{l}"))
# addcmul
lay_g, att_g = g(f"backbone.layer{l}"), g(f"backbone.attention{l}")
xl, att = up(lay_g), up(att_g)
out = ttnn.addcmul(xl.tensor, x.tensor, att.tensor, memory_config=ttnn.DRAM_MEMORY_CONFIG)
dev = fmap_to_numpy(x.with_tensor(out))
free(out, xl, att, x)
e = bf(bf(lay_g) + bf(pfz) * bf(att_g))
report(f"stage{l}(addcmul)", dev, e, g(f"backbone.stage{l}"))
# fuse2
f1 = g("decoder.fuse1")
x = up(f1)
y = net.fuse2(x)
dev = fmap_to_numpy(y)
free(x, y)
report("fuse2", dev, lambda: emu_conv(bf(f1), weights.fuse_layer2), g("decoder.fuse2"))
# element-wise ops on bf16 inputs (synthetic: the stage-1 map scaled to a wide range)
import torch as _t
import ttnn
src = g("backbone.layer1")[:, :, :32, :] * 3.0
xin = up(src)
for opname, fn, ref in (("hardswish", ttnn.hardswish, F.hardswish), ("sigmoid", ttnn.sigmoid,
torch.sigmoid)):
yo = xin.with_tensor(fn(xin.tensor))
dev = fmap_to_numpy(yo)
free(yo)
exact = ref(t(bf(src))).numpy()
report(f"op.{opname}(bf16)", dev, bf(exact), exact)
free(xin)
# softmax (fp32 logits, padded with -1e30)
lg = g("head.logits")[:65536]
pad = np.full((lg.shape[0], LOGIT_PAD), -1e30, np.float32)
pad[:, : lg.shape[1]] = lg
lt = ttnn.from_torch(_t.from_numpy(pad.reshape(1, 1, *pad.shape)), dtype=ttnn.float32,
layout=ttnn.TILE_LAYOUT, device=device, memory_config=ttnn.DRAM_MEMORY_CONFIG)
pr = ttnn.softmax(lt, dim=-1, compute_kernel_config=net._softmax_cfg, numeric_stable=True)
dev = ttnn.to_torch(pr).float().numpy().reshape(-1, LOGIT_PAD)[:, : lg.shape[1]]
free(lt, pr)
exact = torch.softmax(t(lg), -1).numpy()
report("softmax(fp32)", dev, exact, g("pred_probs")[:65536])
net.release()
res["seconds"] = round(time.time() - t0, 1)
results["configs"][f"conv_terms={terms}"] = res
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(json.dumps(results, indent=1))
print("wrote", args.out)
return 0
if __name__ == "__main__":
sys.exit(main())