Skip to content

Commit 6ee104a

Browse files
thomasahleclaude
andcommitted
Second-order regression tests: the compiler must find the algebra
hessian(x^T A x) must compile to the closed form A + A^T (one statement, x never read); hessian(logsumexp) to softmax + diag(p) - pp^T (one softmax, no exp recomputation); the tanh-MLP Hessian computes tanh once. Pinned as KERNEL-COUNT ceilings, not just values, so the cancellation power is a tested invariant. Values checked against torch.func.hessian. Measured (N=1024, CPU 2 threads): quad 0.60ms vs torch.func 3.82ms vs jax jit(hessian) 6.47ms — the frameworks scale ~n^3 (vmap'd double backward), the closed form scales n^2. lse: 0.15 / 0.98 / 0.25ms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 130c430 commit 6ee104a

1 file changed

Lines changed: 79 additions & 0 deletions

File tree

tests/test_second_order.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Second-order derivatives: the compiler must FIND the algebra.
2+
3+
These tests pin kernel-count ceilings, not just values: hessian(x^T A x)
4+
must compile to the closed form A + A^T (no x-compute at all), and
5+
hessian(logsumexp) to softmax + rank-1 correction. torch.func/jax compute
6+
these in O(n^3); the point of symbolic differentiation + consolidation is
7+
that the closed form falls out.
8+
"""
9+
10+
import re
11+
12+
import sympy
13+
import torch
14+
15+
import tensorgrad.functions as F
16+
from tensorgrad import Variable
17+
from tensorgrad.compiler import compile_to_callable
18+
19+
torch.set_num_threads(2)
20+
n = sympy.Symbol("n")
21+
N = 13
22+
23+
24+
def _source_and_out(H, feed, dims):
25+
prog = compile_to_callable(H)
26+
out = prog(feed, dims)
27+
src = next(iter(prog._specializations.values()))._source
28+
stmts = [ln for ln in src.splitlines()
29+
if not ln.lstrip().startswith(("def ", "return", "del ", "#")) and ln.strip()]
30+
return out, stmts
31+
32+
33+
def test_quadratic_hessian_is_closed_form():
34+
x = Variable("x", n)
35+
A = Variable("A", i=n, j=n)
36+
f = (x.rename(n="i") @ A) @ x.rename(n="j")
37+
H = f.grad(x, {"n": "di"}).grad(x, {"n": "dj"})
38+
xv, Av = torch.randn(N), torch.randn(N, N)
39+
out, stmts = _source_and_out(H, {x: xv.rename("n"), A: Av.rename("i", "j")}, {n: N})
40+
with torch.enable_grad():
41+
ref = torch.func.hessian(lambda xx: (xx @ Av @ xx))(xv)
42+
torch.testing.assert_close(out.align_to("di", "dj").rename(None), ref)
43+
# the closed form A + A^T: one statement, and x is never touched
44+
assert len(stmts) <= 2, stmts
45+
assert not any("x" == s.strip() for s in stmts)
46+
47+
48+
def test_logsumexp_hessian_is_softmax_rank1():
49+
x = Variable("x", n)
50+
f = F.log(F.sum(F.exp(x)))
51+
H = f.grad(x, {"n": "di"}).grad(x, {"n": "dj"})
52+
xv = torch.randn(N)
53+
out, stmts = _source_and_out(H, {x: xv.rename("n")}, {n: N})
54+
with torch.enable_grad():
55+
ref = torch.func.hessian(lambda xx: torch.logsumexp(xx, 0))(xv)
56+
torch.testing.assert_close(out.align_to("di", "dj").rename(None), ref)
57+
# softmax + diag-minus-outer: a handful of cheap statements, ONE softmax,
58+
# and crucially no second exp/sum recomputation
59+
assert len(stmts) <= 8, stmts
60+
assert sum("softmax" in s for s in stmts) == 1
61+
assert not any("torch.exp" in s for s in stmts)
62+
63+
64+
def test_mlp_hessian_matches_torch_func():
65+
m = sympy.Symbol("m")
66+
x = Variable("x", n)
67+
W1 = Variable("W1", n=n, m=m)
68+
W2 = Variable("W2", m=m)
69+
f = F.sum(F.tanh(x @ W1) * W2)
70+
H = f.grad(x, {"n": "di"}).grad(x, {"n": "dj"})
71+
xv, W1v, W2v = torch.randn(N), torch.randn(N, 7) / N**0.5, torch.randn(7)
72+
out, stmts = _source_and_out(
73+
H, {x: xv.rename("n"), W1: W1v.rename("n", "m"), W2: W2v.rename("m")}, {n: N, m: 7}
74+
)
75+
with torch.enable_grad():
76+
ref = torch.func.hessian(lambda xx: (torch.tanh(xx @ W1v) * W2v).sum())(xv)
77+
torch.testing.assert_close(out.align_to("di", "dj").rename(None), ref)
78+
# dense Hessian but shared work: tanh computed once, no n^3 contraction
79+
assert sum("tanh" in s for s in stmts) == 1

0 commit comments

Comments
 (0)