|
25 | 25 | dynamic_extract, |
26 | 26 | linear_bin_index, |
27 | 27 | reciprocal, |
| 28 | + table_lookup_2d, |
28 | 29 | ) |
29 | 30 | from torchwright.ops.arithmetic_ops import clamp, subtract |
30 | 31 |
|
| 32 | +# --------------------------------------------------------------------------- |
| 33 | +# table_lookup_2d |
| 34 | +# --------------------------------------------------------------------------- |
| 35 | + |
| 36 | + |
| 37 | +def _table_lookup_2d_reference( |
| 38 | + table: torch.Tensor, |
| 39 | + i: torch.Tensor, |
| 40 | + j: torch.Tensor, |
| 41 | + index_scale=1.0, |
| 42 | + sharpness: float = 100.0, |
| 43 | +) -> torch.Tensor: |
| 44 | + if isinstance(index_scale, (int, float)): |
| 45 | + scale_i = scale_j = float(index_scale) |
| 46 | + else: |
| 47 | + scale_i, scale_j = float(index_scale[0]), float(index_scale[1]) |
| 48 | + rows, cols = table.shape |
| 49 | + eps = 1.0 / sharpness |
| 50 | + max_abs = float(table.abs().max().item()) |
| 51 | + row_slack = max(1e-3, max_abs * sharpness * max(rows, 1) * 1e-6) |
| 52 | + offset = max_abs + row_slack + 1.0 |
| 53 | + |
| 54 | + def _axis_blend(x: float, n: int) -> tuple[int, int, float]: |
| 55 | + idx = max(0, min(n - 1, int(math.floor(x + 0.5)))) |
| 56 | + if n <= 1: |
| 57 | + return idx, idx, 0.0 |
| 58 | + k = int(math.floor(x)) |
| 59 | + boundary = float(k) + 0.5 |
| 60 | + lo = boundary - eps / 2.0 |
| 61 | + hi = boundary + eps / 2.0 |
| 62 | + if 0 <= k <= n - 2 and lo <= x <= hi: |
| 63 | + t = (x - lo) / eps |
| 64 | + return k, k + 1, max(0.0, min(1.0, t)) |
| 65 | + return idx, idx, 0.0 |
| 66 | + |
| 67 | + def _gate(v: float, m: float) -> float: |
| 68 | + return 0.5 * max(v + offset * m, 0.0) - 0.5 * max( |
| 69 | + -v + offset * m, |
| 70 | + 0.0, |
| 71 | + ) |
| 72 | + |
| 73 | + out = torch.empty(i.shape[0], 1, dtype=table.dtype) |
| 74 | + for p in range(i.shape[0]): |
| 75 | + r0, r1, rt = _axis_blend(float(i[p, 0]) * scale_i, rows) |
| 76 | + c0, c1, ct = _axis_blend(float(j[p, 0]) * scale_j, cols) |
| 77 | + |
| 78 | + def _row_value(col: int) -> float: |
| 79 | + return (1.0 - rt) * float(table[r0, col]) + rt * float(table[r1, col]) |
| 80 | + |
| 81 | + if cols == 1: |
| 82 | + out[p, 0] = _row_value(0) |
| 83 | + else: |
| 84 | + left = _row_value(c0) |
| 85 | + right = _row_value(c1) |
| 86 | + out[p, 0] = _gate(left, 1.0 - 2.0 * ct) + _gate( |
| 87 | + right, |
| 88 | + -1.0 + 2.0 * ct, |
| 89 | + ) |
| 90 | + return out |
| 91 | + |
| 92 | + |
| 93 | +def _build_table_lookup_2d_graph(table, index_scale=1.0, sharpness=100.0): |
| 94 | + i = create_input("i", 1) |
| 95 | + j = create_input("j", 1) |
| 96 | + return table_lookup_2d( |
| 97 | + i, |
| 98 | + j, |
| 99 | + table, |
| 100 | + index_scale=index_scale, |
| 101 | + sharpness=sharpness, |
| 102 | + ) |
| 103 | + |
| 104 | + |
| 105 | +def test_table_lookup_2d_every_integer_cell(): |
| 106 | + table = torch.arange(20, dtype=torch.float32).reshape(4, 5) * 3.0 - 7.0 |
| 107 | + out_node = _build_table_lookup_2d_graph(table) |
| 108 | + |
| 109 | + coords = [(r, c) for r in range(table.shape[0]) for c in range(table.shape[1])] |
| 110 | + i_val = torch.tensor([[float(r)] for r, _c in coords], dtype=torch.float32) |
| 111 | + j_val = torch.tensor([[float(c)] for _r, c in coords], dtype=torch.float32) |
| 112 | + inputs = {"i": i_val, "j": j_val} |
| 113 | + n_pos = len(coords) |
| 114 | + |
| 115 | + expected = _table_lookup_2d_reference(table, i_val, j_val) |
| 116 | + cache = reference_eval(out_node, inputs, n_pos) |
| 117 | + assert torch.allclose(cache[out_node], expected, atol=5e-3) |
| 118 | + |
| 119 | + report = probe_graph( |
| 120 | + out_node, |
| 121 | + pos_encoding=None, |
| 122 | + input_values=inputs, |
| 123 | + n_pos=n_pos, |
| 124 | + d=1024, |
| 125 | + d_head=16, |
| 126 | + verbose=False, |
| 127 | + atol=5e-3, |
| 128 | + ) |
| 129 | + assert report.first_divergent is None, report.format_short() |
| 130 | + |
| 131 | + |
| 132 | +def test_table_lookup_2d_scaled_unit_coordinates(): |
| 133 | + table = torch.tensor( |
| 134 | + [ |
| 135 | + [2.0, 3.0, 5.0, 7.0], |
| 136 | + [11.0, 13.0, 17.0, 19.0], |
| 137 | + [23.0, 29.0, 31.0, 37.0], |
| 138 | + ], |
| 139 | + dtype=torch.float32, |
| 140 | + ) |
| 141 | + index_scale = table.shape |
| 142 | + out_node = _build_table_lookup_2d_graph(table, index_scale=index_scale) |
| 143 | + |
| 144 | + coords = [(r, c) for r in range(table.shape[0]) for c in range(table.shape[1])] |
| 145 | + # 0.2 inside each scaled cell keeps the probe well inside the |
| 146 | + # centered integer bin and outside half-integer transition bands. |
| 147 | + i_val = torch.tensor( |
| 148 | + [[(float(r) + 0.2) / table.shape[0]] for r, _c in coords], |
| 149 | + dtype=torch.float32, |
| 150 | + ) |
| 151 | + j_val = torch.tensor( |
| 152 | + [[(float(c) + 0.2) / table.shape[1]] for _r, c in coords], |
| 153 | + dtype=torch.float32, |
| 154 | + ) |
| 155 | + inputs = {"i": i_val, "j": j_val} |
| 156 | + expected = _table_lookup_2d_reference(table, i_val, j_val, index_scale) |
| 157 | + |
| 158 | + cache = reference_eval(out_node, inputs, len(coords)) |
| 159 | + assert torch.allclose(cache[out_node], expected, atol=5e-3) |
| 160 | + |
| 161 | + |
| 162 | +def test_table_lookup_2d_target_size_integer_reference(): |
| 163 | + generator = torch.Generator().manual_seed(0) |
| 164 | + table = torch.rand((128, 128), generator=generator, dtype=torch.float32) * 2.0 - 1.0 |
| 165 | + out_node = _build_table_lookup_2d_graph(table) |
| 166 | + |
| 167 | + coords = [(0, 0), (1, 17), (64, 64), (126, 3), (127, 127)] |
| 168 | + i_val = torch.tensor([[float(r)] for r, _c in coords], dtype=torch.float32) |
| 169 | + j_val = torch.tensor([[float(c)] for _r, c in coords], dtype=torch.float32) |
| 170 | + inputs = {"i": i_val, "j": j_val} |
| 171 | + expected = _table_lookup_2d_reference(table, i_val, j_val) |
| 172 | + |
| 173 | + cache = reference_eval(out_node, inputs, len(coords)) |
| 174 | + assert torch.allclose(cache[out_node], expected, atol=5e-3) |
| 175 | + |
| 176 | + |
| 177 | +def test_table_lookup_2d_near_integer_inputs_remain_in_bin(): |
| 178 | + table = torch.arange(9, dtype=torch.float32).reshape(3, 3) * 4.0 |
| 179 | + out_node = _build_table_lookup_2d_graph(table, sharpness=10.0) |
| 180 | + |
| 181 | + coords = [(0.4, 0.4), (1.4, 1.4), (1.6, 1.6), (-0.4, 2.4)] |
| 182 | + i_val = torch.tensor([[r] for r, _c in coords], dtype=torch.float32) |
| 183 | + j_val = torch.tensor([[c] for _r, c in coords], dtype=torch.float32) |
| 184 | + inputs = {"i": i_val, "j": j_val} |
| 185 | + expected = _table_lookup_2d_reference( |
| 186 | + table, |
| 187 | + i_val, |
| 188 | + j_val, |
| 189 | + sharpness=10.0, |
| 190 | + ) |
| 191 | + |
| 192 | + cache = reference_eval(out_node, inputs, len(coords)) |
| 193 | + assert torch.allclose(cache[out_node], expected, atol=5e-3) |
| 194 | + |
| 195 | + |
| 196 | +def test_table_lookup_2d_accepts_transition_band_inputs(): |
| 197 | + table = torch.tensor([[0.0, 100.0], [20.0, 120.0]], dtype=torch.float32) |
| 198 | + sharpness = 10.0 |
| 199 | + out_node = _build_table_lookup_2d_graph(table, sharpness=sharpness) |
| 200 | + inputs = { |
| 201 | + "i": torch.tensor([[0.5]], dtype=torch.float32), |
| 202 | + "j": torch.tensor([[0.5]], dtype=torch.float32), |
| 203 | + } |
| 204 | + expected = _table_lookup_2d_reference( |
| 205 | + table, |
| 206 | + inputs["i"], |
| 207 | + inputs["j"], |
| 208 | + sharpness=sharpness, |
| 209 | + ) |
| 210 | + |
| 211 | + cache = reference_eval(out_node, inputs, 1) |
| 212 | + assert torch.allclose(cache[out_node], expected, atol=5e-3) |
| 213 | + |
| 214 | + report = probe_graph( |
| 215 | + out_node, |
| 216 | + pos_encoding=None, |
| 217 | + input_values=inputs, |
| 218 | + n_pos=1, |
| 219 | + d=256, |
| 220 | + d_head=16, |
| 221 | + verbose=False, |
| 222 | + atol=5e-3, |
| 223 | + ) |
| 224 | + assert report.first_divergent is None, report.format_short() |
| 225 | + |
31 | 226 | # --------------------------------------------------------------------------- |
32 | 227 | # dynamic_extract |
33 | 228 | # --------------------------------------------------------------------------- |
|
0 commit comments