File size: 8,413 Bytes
d160c10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# SPDX-License-Identifier: Apache-2.0
"""Probe (OPT round 2, item 7a): the image encoder's 1x1 / stride-1 convs as ``ttnn.linear`` with explicit program
configs against ``ttnn.conv2d``'s own matmul (C17 ``Conv2d``), per shape: bit identity of the bf16 output and the
device time of each candidate (trace replays of 20 calls, b2b). Candidates keep the stock K blocking
(``in0_block_w``), so the fp32 K order is the same; ReLU is fused in the config (packer) where the conv has one.

    bin/devrun -t 1200 -- python -u code/scripts/probe_mm.py --json logs/bevdet/opt_r2/probe_mm.json
"""
from __future__ import annotations

import argparse
import itertools
import json
import sys
import time
from pathlib import Path

import numpy as np

# (rows, K, N, relu, stock kind, stock kb): the encoder's 1x1 stride-1 convs (OPT round 1 profile)
SHAPES = [
    (67584, 64, 64, True, "1d", 2),       # layer1.0 conv1
    (67584, 64, 256, False, "1d", 2),     # layer1 conv3 / layer1.0 downsample
    (67584, 256, 64, True, "1d", 2),      # layer1 conv1
    (67584, 256, 128, True, "1d", 2),     # layer2.0 conv1
    (16896, 128, 512, False, "1d", 2),    # layer2 conv3
    (16896, 512, 128, True, "1d", 2),     # layer2 conv1
    (16896, 512, 256, True, "1d", 2),     # layer3.0 conv1
    (4224, 256, 1024, False, "2d", 1),    # layer3 conv3
    (4224, 1024, 256, True, "1d", 2),     # layer3 conv1
    (4224, 1024, 512, True, "2d", 1),     # layer4.0 conv1 / lat0
    (1056, 512, 2048, False, "2d", 1),    # layer4 conv3
    (1056, 2048, 512, True, "2d", 1),     # layer4 conv1 / lat1
    (16384, 256, 256, False, "1d", 2),    # [12] decoder neck up2.4 (out_conv; OPT round 5)
]


def divisors(n):
    return [d for d in range(1, n + 1) if n % d == 0]


def subblocks(obh, obw):
    out = []
    for sh, sw in [(2, 2), (1, 4), (4, 1), (1, 2), (2, 1), (1, 1)]:
        if obh % sh == 0 and obw % sw == 0:
            out.append((sh, sw))
    return out[:1]


def candidates(m, k, n, kb, gx, gy):
    import ttnn

    mt, nt, kt = m // 32, n // 32, k // 32
    out = []
    # 1D (in1 multicast, rows split over all cores)
    pcm = -(-mt // (gx * gy))
    for obh in sorted({d for d in divisors(pcm) if d >= max(1, pcm // 6)} | {pcm}):
        for obw in sorted({d for d in divisors(nt)}):
            if obh * obw > 96 or obh * obw < 4 and not (obh == pcm and obw == nt):
                continue
            for sh, sw in subblocks(obh, obw):
                out.append(("1d", kb, obh, obw, sh, sw))
    # 2D (block multicast)
    for cx in [d for d in divisors(nt) if d <= gx]:
        pm = -(-mt // gy)
        pn = nt // cx
        for obh in sorted({d for d in divisors(pm) if d >= max(1, pm // 8)}):
            if obh * pn > 96:
                continue
            for sh, sw in subblocks(obh, pn):
                out.append(("2d", kb, cx, obh, sh, sw))
    return out


def make_pc(c, m, n, gx, gy, relu):
    import ttnn

    mt, nt = m // 32, n // 32
    act = ttnn.UnaryWithParam(ttnn.UnaryOpType.RELU) if relu else None
    if c[0] == "1d":
        _, kb, obh, obw, sh, sw = c
        return ttnn.MatmulMultiCoreReuseMultiCast1DProgramConfig(
            compute_with_storage_grid_size=(gx, gy), in0_block_w=kb, out_subblock_h=sh, out_subblock_w=sw,
            out_block_h=obh, out_block_w=obw, per_core_M=-(-mt // (gx * gy)), per_core_N=nt, fuse_batch=True,
            fused_activation=act, mcast_in0=False)
    _, kb, cx, obh, sh, sw = c
    pm = -(-mt // gy)
    return ttnn.MatmulMultiCoreReuseMultiCastProgramConfig(
        compute_with_storage_grid_size=(cx, gy), in0_block_w=kb, out_subblock_h=sh, out_subblock_w=sw,
        out_block_h=obh, out_block_w=nt // cx, per_core_M=pm, per_core_N=nt // cx, transpose_mcast=False,
        fused_activation=act)


def bits(t):
    import torch
    import ttnn

    return ttnn.to_torch(ttnn.from_device(t)).to(torch.bfloat16).view(torch.int16).numpy()


def timed(device, fn, reps=20):
    """Device time per call (ms): one trace of ``reps`` calls, replayed 3 times, best replay."""
    import ttnn

    outs = []
    fn()  # compile
    ttnn.synchronize_device(device)
    tid = ttnn.begin_trace_capture(device, cq_id=0)
    for _ in range(reps):
        outs.append(fn())
    ttnn.end_trace_capture(device, tid, cq_id=0)
    best = 1e9
    for _ in range(3):
        ttnn.synchronize_device(device)
        t0 = time.perf_counter()
        ttnn.execute_trace(device, tid, cq_id=0, blocking=True)
        best = min(best, (time.perf_counter() - t0) * 1e3 / reps)
    ttnn.release_trace(device, tid)
    for o in outs:
        ttnn.deallocate(o)
    return best


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--json")
    ap.add_argument("--only", default="", help="comma list of shape indices")
    a = ap.parse_args()
    import torch
    import ttnn

    from tt_bevdet.device import close_device, open_device
    from tt_bevdet.tt.params import DEFAULT_PRECISION
    from tt_bevdet.ttaw.ops.conv import Conv2d, FeatureMap
    from tt_bevdet.ttaw.precision import Precision

    dev = open_device(trace_region_size=512 << 20)
    g = dev.compute_with_storage_grid_size()
    gx, gy = int(g.x), int(g.y)
    prec = Precision.parse(DEFAULT_PRECISION)
    cfg = prec.compute_kernel_config()
    rng = np.random.default_rng(0)
    res = []
    only = {int(v) for v in a.only.split(",") if v.strip()}
    try:
        for si, (m, k, n, relu, kind, kb) in enumerate(SHAPES):
            if only and si not in only:
                continue
            x = rng.standard_normal((1, 1, m, k)).astype(np.float32)
            w = (rng.standard_normal((n, k, 1, 1)) / np.sqrt(k)).astype(np.float32)
            b = rng.standard_normal(n).astype(np.float32) * 0.1
            xt = ttnn.from_torch(torch.from_numpy(x), dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT, device=dev,
                                 memory_config=ttnn.DRAM_MEMORY_CONFIG)
            conv = Conv2d(w, b, padding=0, activation="relu" if relu else None, precision=DEFAULT_PRECISION,
                          name=f"s{si}")
            fm = FeatureMap(xt, 1, m, 1, k)
            ref_t = conv(fm).tensor
            ref = bits(ref_t)
            ttnn.deallocate(ref_t)
            t_stock = timed(dev, lambda: conv(fm).tensor)
            wt = ttnn.from_torch(torch.from_numpy(np.ascontiguousarray(w[:, :, 0, 0].T)), dtype=ttnn.float32,
                                 layout=ttnn.TILE_LAYOUT, device=dev, memory_config=ttnn.DRAM_MEMORY_CONFIG)
            bt = ttnn.from_torch(torch.from_numpy(b.reshape(1, -1)), dtype=ttnn.float32, layout=ttnn.TILE_LAYOUT,
                                 device=dev, memory_config=ttnn.DRAM_MEMORY_CONFIG)
            row = {"shape": [m, k, n], "relu": relu, "stock_ms": round(t_stock, 4), "cands": []}
            print(f"[{si}] {m}x{k}x{n} relu={relu}: stock {t_stock:.4f} ms", flush=True)
            best = None
            for c in candidates(m, k, n, kb, gx, gy):
                try:
                    pc = make_pc(c, m, n, gx, gy, relu)

                    def run():
                        return ttnn.linear(xt, wt, bias=bt, program_config=pc, compute_kernel_config=cfg,
                                           memory_config=ttnn.DRAM_MEMORY_CONFIG, dtype=ttnn.bfloat16)
                    o = run()
                    got = bits(o)
                    ttnn.deallocate(o)
                    nbad = int(np.count_nonzero(got != ref))
                    t = timed(dev, run)
                except Exception as e:  # noqa: BLE001 - config rejected / L1 clash
                    row["cands"].append({"cfg": list(c), "error": str(e).splitlines()[0][:160]})
                    continue
                row["cands"].append({"cfg": list(c), "ms": round(t, 4), "bad": nbad})
                print(f"    {c}: {t:.4f} ms, {nbad} differ", flush=True)
                if nbad == 0 and (best is None or t < best[1]):
                    best = (c, t)
            row["best_exact"] = None if best is None else {"cfg": list(best[0]), "ms": round(best[1], 4)}
            print(f"  best exact: {row['best_exact']}", flush=True)
            res.append(row)
            for t in (xt, wt, bt):
                ttnn.deallocate(t)
            conv.release()
    finally:
        close_device(dev)
    if a.json:
        Path(a.json).write_text(json.dumps(res, indent=1))
    return 0


if __name__ == "__main__":
    sys.exit(main())