Skip to content

Commit 255413e

Browse files
committed
Add differentiation-through-aggregate tests and docs
Document and test that differentiating through SUM/AVG is just linearity: AGG(grad(f, x)) == d/dx AGG(f). Writing grad inside the aggregate composes with SQL scoping (the marker rewrites to plain SQL before the aggregate runs), so it needs no special machinery -- enough to express gradient descent in SQL. Adds tests for SUM/AVG(grad(...)) and an end-to-end gradient-descent convergence test, plus a note in the module overview. The runnable benchmark scripts live on stacked demo branches to keep this feature branch reviewable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017mDoFJgsm9kS7SicGoCVF6
1 parent bdad6fb commit 255413e

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

src/autograd.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@
4040
//! Calls nest, giving higher-order derivatives for free: the rewrite walks
4141
//! bottom-up, so the inner call in `grad(grad(f, x), x)` is differentiated
4242
//! first and the outer call differentiates that result.
43+
//!
44+
//! Differentiation through an aggregate is just linearity and needs no special
45+
//! handling: write the `grad` *inside* the aggregate, e.g. `SUM(grad(f, x))` or
46+
//! `AVG(grad(loss, theta))`. Because the marker is rewritten to plain SQL
47+
//! before the aggregate runs (and the column is in scope there), this is the
48+
//! relational `d/dθ Σ f = Σ ∂f/∂θ` — enough to run gradient descent in SQL.
49+
//! (The transposed form `grad(SUM(f), x)` is rejected by SQL's own scoping,
50+
//! since `x` is gone after aggregation.)
4351
4452
#![allow(dead_code)]
4553

tests/test_autograd.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,58 @@ def test_unsupported_function_raises(ctx):
117117
ctx.sql("SELECT grad(atan2(val, val), val) AS d FROM t").to_pandas()
118118

119119

120+
def test_grad_inside_aggregate(ctx):
121+
# Differentiation through an aggregate is just linearity:
122+
# AGG(grad(f, x)) == d/dx AGG(f). grad rewrites to plain SQL before the
123+
# aggregate runs, so this composes with no special machinery.
124+
val = np.linspace(0.1, 3.0, 16)
125+
res = ctx.sql(
126+
"SELECT SUM(grad(val * val, val)) AS s, "
127+
"AVG(grad(sin(val), val)) AS a FROM t"
128+
).to_pandas()
129+
np.testing.assert_allclose(res["s"][0], np.sum(2 * val))
130+
np.testing.assert_allclose(res["a"][0], np.mean(np.cos(val)))
131+
132+
133+
def test_gradient_descent_in_sql():
134+
# End to end: fit y ~= a*x + b by minimising MSE, with the gradients
135+
# w.r.t. the parameters computed in SQL via AVG(grad(loss, param)).
136+
rng = np.random.default_rng(0)
137+
n = 200
138+
x = rng.uniform(0.0, 1.0, n)
139+
a_true, b_true = 2.0, -1.0
140+
y = a_true * x + b_true + rng.normal(0.0, 0.01, n)
141+
data = xr.Dataset(
142+
{"x": (("i",), x), "y": (("i",), y)}, coords={"i": np.arange(n)}
143+
)
144+
ctx = xql.XarrayContext()
145+
ctx.from_dataset("d", data, chunks={"i": n})
146+
147+
resid = "(y - (a * x + b))"
148+
loss = f"{resid} * {resid}"
149+
a, b, lr = 0.0, 0.0, 0.4
150+
losses = []
151+
for _ in range(120):
152+
if "params" in ctx._registered_datasets:
153+
ctx.deregister_table("params")
154+
del ctx._registered_datasets["params"]
155+
params = xr.Dataset(
156+
{"a": (("p",), [a]), "b": (("p",), [b])}, coords={"p": [0]}
157+
)
158+
ctx.from_dataset("params", params, chunks={"p": 1})
159+
row = ctx.sql(
160+
f"SELECT AVG({loss}) AS loss, "
161+
f"AVG(grad({loss}, a)) AS dl_da, "
162+
f"AVG(grad({loss}, b)) AS dl_db FROM d CROSS JOIN params"
163+
).to_pandas()
164+
losses.append(float(row["loss"][0]))
165+
a -= lr * float(row["dl_da"][0])
166+
b -= lr * float(row["dl_db"][0])
167+
168+
assert losses[-1] < losses[0] # loss decreased
169+
np.testing.assert_allclose([a, b], [a_true, b_true], atol=0.05)
170+
171+
120172
def test_multi_input_grad_columns(ctx_xy):
121173
# A full Jacobian written as separate scalar grad() columns:
122174
# f = x*y -> df/dx = y, df/dy = x.

0 commit comments

Comments
 (0)