Skip to content

Commit b5618f4

Browse files
committed
Add centered 2D table lookup primitive
1 parent 9a6d2a7 commit b5618f4

3 files changed

Lines changed: 423 additions & 6 deletions

File tree

tests/ops/test_resampling_primitives.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,204 @@
2525
dynamic_extract,
2626
linear_bin_index,
2727
reciprocal,
28+
table_lookup_2d,
2829
)
2930
from torchwright.ops.arithmetic_ops import clamp, subtract
3031

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+
31226
# ---------------------------------------------------------------------------
32227
# dynamic_extract
33228
# ---------------------------------------------------------------------------

torchwright/ops/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
map_to_table,
5454
select,
5555
switch,
56+
table_lookup_2d,
5657
)
5758

5859
# I/O nodes

0 commit comments

Comments
 (0)