File size: 8,655 Bytes
4d9b003
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be62f78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# SPDX-License-Identifier: Apache-2.0
"""The exact rewrites of ``tt/params.py`` and the input packing of ``tt/inputs.py`` against the CPU reference
(float64, no device): every constant the ttnn graph holds is the reference's math rearranged.

    TT_VISIBLE_DEVICES=none python -m pytest -q -p fake_ttnn_plugin code/tt_diffusion_planner/tests/test_tt_params_host.py
"""
from __future__ import annotations

import numpy as np
import pytest

from tt_diffusion_planner.host import pipeline as hp
from tt_diffusion_planner.host.solver import apply_prefix_constraint, dpm_solver_sample
from tt_diffusion_planner.reference import config as C
from tt_diffusion_planner.tt import config as T
from tt_diffusion_planner.tt import inputs as I
from tt_diffusion_planner.tt import params as P

torch = pytest.importorskip("torch")
F = torch.nn.functional
PKG = __import__("pathlib").Path(__file__).resolve().parents[1]


@pytest.fixture(scope="module")
def weights():
    from tt_diffusion_planner.reference.weights import find_weights_dir, load_weights

    wd = find_weights_dir()
    if wd is None:
        pytest.skip("Diffusion Planner v5.0 weights not found")
    return load_weights(wd)


@pytest.fixture(scope="module")
def prepared(weights):
    from tt_diffusion_planner.ttaw.io import load_named_arrays

    raw = load_named_arrays(str(PKG / "samples" / "kashiwanoha_dense.npz"), C.INPUT_SCHEMA)
    return hp.prepare(raw, weights.normalization.observation)


def d(a):
    return np.asarray(a, np.float64)


def test_lane_aux_is_the_speed_and_attribute_embeddings(weights):
    p = weights.params
    rng = np.random.default_rng(0)
    for cat, mod in (("lane", "encoder.lane_encoder"), ("route", "encoder.route_encoder")):
        e = 40
        speed = rng.random(e).astype(np.float32) * 2
        has = rng.random(e) < 0.6
        speed[~has] = 0.0
        attr = (rng.random((e, C.LANE_ATTRIBUTE_DIM)) < 0.2).astype(np.float32)
        got = d(P.lane_aux_features(speed, has, attr)) @ d(P.lane_aux(p, cat))
        lin = d(speed)[:, None] @ d(p[f"{mod}.speed_limit_emb.w"]) + d(p[f"{mod}.speed_limit_emb.b"])
        want = np.where(has[:, None], lin, d(p[f"{mod}.unknown_speed_emb"])[None]) \
            + d(attr) @ d(p[f"{mod}.attribute_emb.w"]) + d(p[f"{mod}.attribute_emb.b"])
        np.testing.assert_allclose(got, want, rtol=1e-12, atol=1e-12)


def test_neighbor_aux_and_pos_aug(weights, prepared):
    p = weights.params
    f = prepared.features
    t = d(f.neighbor_type)
    got = np.concatenate([t, np.ones((t.shape[0], 1))], 1) @ d(P.neighbor_aux(p))
    want = t @ d(p["encoder.neighbor_encoder.type_emb.w"]) + d(p["encoder.neighbor_encoder.type_emb.b"])
    np.testing.assert_allclose(got, want, rtol=1e-12, atol=1e-12)
    inp = I.plan_inputs(prepared)
    pos = d(inp["pos_aug"][0, 0]) @ d(P.pos_aug(p))
    tv = np.asarray(f.token_valid, bool)
    ref = (d(f.pos) @ d(p["encoder.pos_emb.w"]) + d(p["encoder.pos_emb.b"])) * tv[:, None]
    np.testing.assert_allclose(pos[:T.TOKENS_REAL], ref, rtol=1e-12, atol=1e-12)
    assert not pos[T.TOKENS_REAL:].any()


def test_agent_rows_and_masked_projection(weights):
    p = weights.params
    rows = P.agent_rows(p)
    emb = d(p["decoder.dit.agent_embedding"])
    b2 = d(p["decoder.dit.preproj.fc2.b"])
    np.testing.assert_allclose(d(rows[0]), b2 + emb[0], rtol=1e-6)
    np.testing.assert_allclose(d(rows[1:]), np.repeat((b2 + emb[1])[None], T.AGENTS - 1, 0), rtol=1e-6)
    lin = P.final_projection_masked(p)
    assert not lin.w[:, :4].any() and not lin.b[:4].any()
    np.testing.assert_array_equal(lin.w[:, 4:], np.asarray(p["decoder.dit.final_layer.proj.4.w"])[:, 4:])


def test_turn_weights_reproduce_the_head(weights):
    from tt_diffusion_planner.reference.model import TurnHead, torch_params

    p = weights.params
    rng = np.random.default_rng(1)
    enc = rng.standard_normal((T.TOKENS_REAL, C.HIDDEN_DIM))
    x0 = rng.standard_normal((C.MAX_NUM_AGENTS, C.OUTPUT_T + 1, C.POSE_DIM))
    w = P.turn_weights(p)
    got = x0[0].reshape(-1) @ d(w["w_sel"]) + enc.sum(0) @ d(w["w_pool"]) + d(w["b"])
    head = TurnHead(torch_params(p, torch.float64))
    want = head.forward(torch.from_numpy(enc), x0).numpy()
    np.testing.assert_allclose(got, want, rtol=1e-6, atol=1e-6)


def test_step_tables_and_solver_recursion(weights):
    """The y-recursion with (A, B, Cm) and the masked model output equals the node's DPM-Solver++(2M) with the
    prefix constraint (dpm_solver_sample) on a deterministic stand-in model."""
    tab = P.step_tables(weights.params)
    assert tab.nfe == C.DPM_SOLVER_STEPS + 1 and len(tab.solver) == C.DPM_SOLVER_STEPS
    assert len(tab.blocks) == tab.nfe and len(tab.blocks[0]) == C.DIT_DEPTH
    rng = np.random.default_rng(2)
    agents = 5
    W = rng.standard_normal((324, 324)).astype(np.float32) * 0.05
    cs = rng.standard_normal((agents, 4)).astype(np.float32)
    x_T = rng.standard_normal((agents, 81, 4)).astype(np.float32)

    def model(x, t):
        return np.tanh(x.reshape(agents, -1) @ W + float(t)).reshape(agents, 81, 4).astype(np.float32)

    ref = dpm_solver_sample(x_T, model, lambda x: apply_prefix_constraint(x, cs))
    mask = np.ones((81, 4), np.float32)
    mask[0] = 0
    y = (x_T * mask).astype(np.float64)
    m_prev = None
    iters = []
    for k in range(tab.nfe):
        x = y + np.concatenate([cs[:, None], np.zeros((agents, 80, 4))], 1)
        if k > 0:
            iters.append(x)
        m = model(x.astype(np.float32), tab.eval_times[k]).astype(np.float64) * mask
        if k == tab.nfe - 1:
            break
        a, b, c = tab.solver[k]
        y = a * y - b * m + (c * m_prev if m_prev is not None else 0.0)
        m_prev = m
    final = m + np.concatenate([cs[:, None], np.zeros((agents, 80, 4))], 1)
    iters.append(final)
    np.testing.assert_allclose(final, ref.final_x, rtol=1e-5, atol=1e-5)
    for got, want in zip(iters, ref.denoising_steps):
        np.testing.assert_allclose(got, want, rtol=1e-5, atol=1e-5)
    assert np.allclose(tab.eval_times, ref.eval_times)


def test_plan_inputs_layout(prepared):
    inp = I.plan_inputs(prepared)
    assert set(inp) == set(I.INPUT_SPECS)
    f = prepared.features
    np.testing.assert_array_equal(inp["neighbor_x"][0], f.neighbor[:, 25:31])
    assert not f.neighbor[:, :25].any() and not f.ego[6:].any()        # the island's zero rows
    np.testing.assert_array_equal(inp["ego_x"][0, 0], f.ego[:6])
    row = inp["fusion_key_row"][0, 0, 0]
    assert np.isneginf(row[T.TOKENS_REAL:]).all() and (row[:T.TOKENS_REAL][f.key_valid] == 0).all()
    arow = inp["agent_key_row"][0, 0, 0]
    assert np.isneginf(arow[C.MAX_NUM_AGENTS:]).all() and arow[0] == 0
    cs, y0 = inp["cs"][0, 0], inp["y0"][0, 0]
    np.testing.assert_array_equal(cs[:C.MAX_NUM_AGENTS, :4], prepared.decoder.current_states)
    assert not cs[:, 4:].any() and not y0[:, :4].any() and not cs[C.MAX_NUM_AGENTS:].any()
    np.testing.assert_array_equal(y0[:C.MAX_NUM_AGENTS, 4:], prepared.x_T.reshape(C.MAX_NUM_AGENTS, -1)[:, 4:])
    warm = I.warmup_inputs()
    assert all(warm[k].shape == v for k, v in I.INPUT_SPECS.items())


def test_compact_bucket_covers_every_needed_row():
    """``COMPACT``: the bucket holds the ego, every valid self-attention key and every emitted neighbour row (the
    rows past it are masked keys, never read back), and falls back to the full 352 rows."""
    from types import SimpleNamespace

    from tt_diffusion_planner.tt.model import bucket_rows, needed_rows

    def prep(valid_idx, emitted):
        v = np.zeros(C.MAX_NUM_AGENTS, bool)
        v[0] = True
        v[list(valid_idx)] = True
        nb = np.zeros(C.MAX_NUM_NEIGHBORS, bool)
        nb[[i - 1 for i in valid_idx]] = True
        return SimpleNamespace(decoder=SimpleNamespace(agent_valid=v), neighbor_rows=np.asarray(emitted, int),
                               features=SimpleNamespace(valid={"neighbor": nb}))

    b = (32, 64, 96, 128, 192)
    assert needed_rows(prep([], [])) == 1 and bucket_rows(prep([], []), b) == 32
    assert needed_rows(prep([1, 2, 31], [0, 1, 30])) == 32 and bucket_rows(prep([31], []), b) == 32
    assert bucket_rows(prep([32], []), b) == 64                      # the 33rd row is a valid key
    assert bucket_rows(prep([5], [70]), b) == 96                     # emitted neighbour 70 = decoder row 71
    assert bucket_rows(prep([200], []), b) == T.AGENTS
    assert bucket_rows(prep([5], []), ()) == T.AGENTS                # COMPACT off
    p = prep([3], [])
    p.features.valid["neighbor"][40] = True                          # a valid neighbour token (row 41)
    assert needed_rows(p) == 42 and bucket_rows(p, b) == 64