Skip to content

Commit 8a37b4c

Browse files
thomasahleclaude
andcommitted
benchmarks/: persistent second-order harnesses (tg vs torch.func vs jax)
The numbers behind tests/test_second_order.py's pinned cancellations, runnable for the paper: second_order.py (correctness + kernel counts + timing vs torch.func), second_order_scale.py (N sweep), second_order_jax.py (uv run --with jax). Measured this session: quad hessian 0.60ms vs 3.82 (torch.func) vs 6.47 (jax jit) at N=1024. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 6ee104a commit 8a37b4c

3 files changed

Lines changed: 192 additions & 0 deletions

File tree

benchmarks/second_order.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""Second-order derivatives: tensorgrad vs torch.func (correctness + timing).
2+
3+
Cases chosen for known algebraic structure the compiler should FIND:
4+
quad: f = x^T A x -> H = A + A^T (constant! no x-compute)
5+
lse: f = logsumexp(x) -> H = diag(p) - p p^T (softmax structure)
6+
mlp: f = sum(tanh(x W1) W2) -> dense but shareable
7+
Each: correctness vs torch.func.hessian, then wall time and emitted-kernel
8+
count for the tensorgrad program.
9+
"""
10+
11+
import re
12+
import sys
13+
import time
14+
15+
import sympy
16+
import torch
17+
18+
torch.set_grad_enabled(False)
19+
torch.set_num_threads(2)
20+
21+
import tensorgrad.functions as F
22+
from tensorgrad import Variable
23+
from tensorgrad.compiler import compile_to_callable
24+
25+
n = sympy.Symbol("n")
26+
N = 64
27+
DIMS = {n: N}
28+
29+
30+
def kernels(prog):
31+
fn = next(iter(prog._specializations.values()))
32+
src = fn._source
33+
ops = re.findall(r"torch\.\w+\(|\.sum\(|\.argsort\(", src)
34+
return len(ops), src
35+
36+
37+
def bench(f, reps=50):
38+
for _ in range(5):
39+
f()
40+
t0 = time.perf_counter()
41+
for _ in range(reps):
42+
f()
43+
return (time.perf_counter() - t0) / reps * 1e3 # ms
44+
45+
46+
results = []
47+
48+
# ---- case 1: quadratic --------------------------------------------------
49+
x = Variable("x", n)
50+
A = Variable("A", i=n, j=n)
51+
f_quad = F.dot(x.rename(n="i") @ A, x.rename(n="j"), dim="j") if False else (
52+
(x.rename(n="i") @ A) @ x.rename(n="j")
53+
)
54+
# f = sum_ij x_i A_ij x_j (scalar)
55+
g = f_quad.grad(x, {"n": "di"})
56+
H = g.grad(x, {"n": "dj"})
57+
prog = compile_to_callable(H)
58+
xv = torch.randn(N)
59+
Av = torch.randn(N, N)
60+
with torch.enable_grad():
61+
Ht = torch.func.hessian(lambda xx: (xx @ Av @ xx))(xv)
62+
out = prog({x: xv.rename("n"), A: Av.rename("i", "j")}, DIMS)
63+
got = out.align_to("di", "dj").rename(None)
64+
assert torch.allclose(got, Ht, atol=1e-4), (got - Ht).abs().max()
65+
k, src = kernels(prog)
66+
t_tg = bench(lambda: prog({x: xv.rename("n"), A: Av.rename("i", "j")}, DIMS))
67+
with torch.enable_grad():
68+
t_torch = bench(lambda: torch.func.hessian(lambda xx: (xx @ Av @ xx))(xv))
69+
results.append(("quad (H = A+A^T)", k, t_tg, t_torch))
70+
print("=== quad generated source ===")
71+
print(src)
72+
73+
# ---- case 2: logsumexp --------------------------------------------------
74+
f_lse = F.log(F.sum(F.exp(x)))
75+
H2 = f_lse.grad(x, {"n": "di"}).grad(x, {"n": "dj"})
76+
prog2 = compile_to_callable(H2)
77+
with torch.enable_grad():
78+
Ht2 = torch.func.hessian(lambda xx: torch.logsumexp(xx, 0))(xv)
79+
out2 = prog2({x: xv.rename("n")}, DIMS)
80+
got2 = out2.align_to("di", "dj").rename(None)
81+
assert torch.allclose(got2, Ht2, atol=1e-4), (got2 - Ht2).abs().max()
82+
k2, src2 = kernels(prog2)
83+
t_tg2 = bench(lambda: prog2({x: xv.rename("n")}, DIMS))
84+
with torch.enable_grad():
85+
t_torch2 = bench(lambda: torch.func.hessian(lambda xx: torch.logsumexp(xx, 0))(xv))
86+
results.append(("lse (H = diag(p)-pp^T)", k2, t_tg2, t_torch2))
87+
print("=== lse generated source ===")
88+
print(src2)
89+
90+
# ---- case 3: 2-layer MLP ------------------------------------------------
91+
m = sympy.Symbol("m")
92+
M = 64
93+
W1 = Variable("W1", n=n, m=m)
94+
W2 = Variable("W2", m=m)
95+
f_mlp = F.sum(F.tanh(x @ W1) * W2)
96+
H3 = f_mlp.grad(x, {"n": "di"}).grad(x, {"n": "dj"})
97+
prog3 = compile_to_callable(H3)
98+
W1v, W2v = torch.randn(N, M) / N**0.5, torch.randn(M)
99+
with torch.enable_grad():
100+
Ht3 = torch.func.hessian(lambda xx: (torch.tanh(xx @ W1v) * W2v).sum())(xv)
101+
feed3 = {x: xv.rename("n"), W1: W1v.rename("n", "m"), W2: W2v.rename("m")}
102+
out3 = prog3(dict(feed3), DIMS | {m: M})
103+
got3 = out3.align_to("di", "dj").rename(None)
104+
assert torch.allclose(got3, Ht3, atol=1e-4), (got3 - Ht3).abs().max()
105+
k3, _ = kernels(prog3)
106+
t_tg3 = bench(lambda: prog3(dict(feed3), DIMS | {m: M}))
107+
with torch.enable_grad():
108+
t_torch3 = bench(lambda: torch.func.hessian(lambda xx: (torch.tanh(xx @ W1v) * W2v).sum())(xv))
109+
results.append(("mlp tanh", k3, t_tg3, t_torch3))
110+
111+
print()
112+
print(f"{'case':26s} {'tg kernels':>10s} {'tg ms':>8s} {'torch.func ms':>13s} {'speedup':>8s}")
113+
for name, k, a, b in results:
114+
print(f"{name:26s} {k:10d} {a:8.3f} {b:13.3f} {b / a:7.1f}x")

benchmarks/second_order_jax.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""JAX baseline for the second-order benches: uv run --with jax python benchmarks/second_order_jax.py"""
2+
import os, time
3+
os.environ["XLA_FLAGS"] = "--xla_cpu_multi_thread_eigen=false intra_op_parallelism_threads=2"
4+
os.environ["OMP_NUM_THREADS"] = "2"
5+
import jax, jax.numpy as jnp
6+
import numpy as np
7+
8+
def bench(f, budget_s=2.0):
9+
f()
10+
t0 = time.perf_counter(); reps = 0
11+
while time.perf_counter() - t0 < budget_s:
12+
f(); reps += 1
13+
return (time.perf_counter() - t0) / reps * 1e3
14+
15+
print(f"{'case':8s} {'N':>5s} {'jax ms':>9s}")
16+
for N in (64, 256, 1024):
17+
key = jax.random.PRNGKey(0)
18+
xv = jax.random.normal(key, (N,)); Av = jax.random.normal(key, (N, N))
19+
hq = jax.jit(jax.hessian(lambda xx: xx @ Av @ xx))
20+
r = hq(xv); r.block_until_ready()
21+
t = bench(lambda: hq(xv).block_until_ready())
22+
print(f"{'quad':8s} {N:5d} {t:9.3f}")
23+
for N in (64, 256, 1024):
24+
xv = jax.random.normal(jax.random.PRNGKey(1), (N,))
25+
hl = jax.jit(jax.hessian(lambda xx: jax.scipy.special.logsumexp(xx)))
26+
hl(xv).block_until_ready()
27+
t = bench(lambda: hl(xv).block_until_ready())
28+
print(f"{'lse':8s} {N:5d} {t:9.3f}")

benchmarks/second_order_scale.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Scale sweep: tensorgrad vs torch.func on Hessians, N in {64, 256, 1024}."""
2+
3+
import sys
4+
import time
5+
6+
import sympy
7+
import torch
8+
9+
torch.set_grad_enabled(False)
10+
torch.set_num_threads(2)
11+
import tensorgrad.functions as F
12+
from tensorgrad import Variable
13+
from tensorgrad.compiler import compile_to_callable
14+
15+
n = sympy.Symbol("n")
16+
17+
18+
def bench(f, budget_s=2.0):
19+
f()
20+
t0 = time.perf_counter()
21+
reps = 0
22+
while time.perf_counter() - t0 < budget_s:
23+
f()
24+
reps += 1
25+
return (time.perf_counter() - t0) / reps * 1e3
26+
27+
28+
x = Variable("x", n)
29+
A = Variable("A", i=n, j=n)
30+
f_quad = (x.rename(n="i") @ A) @ x.rename(n="j")
31+
H_quad = f_quad.grad(x, {"n": "di"}).grad(x, {"n": "dj"})
32+
prog_quad = compile_to_callable(H_quad)
33+
34+
f_lse = F.log(F.sum(F.exp(x)))
35+
H_lse = f_lse.grad(x, {"n": "di"}).grad(x, {"n": "dj"})
36+
prog_lse = compile_to_callable(H_lse)
37+
38+
print(f"{'case':8s} {'N':>5s} {'tg ms':>9s} {'torch ms':>9s} {'speedup':>8s}")
39+
for N in (64, 256, 1024):
40+
xv, Av = torch.randn(N), torch.randn(N, N)
41+
t_tg = bench(lambda: prog_quad({x: xv.rename("n"), A: Av.rename("i", "j")}, {n: N}))
42+
with torch.enable_grad():
43+
t_t = bench(lambda: torch.func.hessian(lambda xx: (xx @ Av @ xx))(xv))
44+
print(f"{'quad':8s} {N:5d} {t_tg:9.3f} {t_t:9.3f} {t_t / t_tg:7.1f}x")
45+
for N in (64, 256, 1024):
46+
xv = torch.randn(N)
47+
t_tg = bench(lambda: prog_lse({x: xv.rename("n")}, {n: N}))
48+
with torch.enable_grad():
49+
t_t = bench(lambda: torch.func.hessian(lambda xx: torch.logsumexp(xx, 0))(xv))
50+
print(f"{'lse':8s} {N:5d} {t_tg:9.3f} {t_t:9.3f} {t_t / t_tg:7.1f}x")

0 commit comments

Comments
 (0)