|
| 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") |
0 commit comments