Skip to content

Commit a4fc101

Browse files
alxmrsclaude
andcommitted
demo: use grad() inside the recursive CTE for gradient descent
Now that grad() is differentiated as a SQL rewrite before planning, it works inside a recursive CTE — so the gradient-descent demo no longer needs the differentiate_sql precompile step. The loss is written once and grad() is differentiated in place within the recursion, making the whole training loop a single declarative query with the marker in it. Updates the benchmarks README rationale to match (the old "grad can't live in recursion via Substrait" note is no longer true). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d6d8cfb commit a4fc101

2 files changed

Lines changed: 56 additions & 59 deletions

File tree

benchmarks/README.md

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,30 @@ Each query round-trips back to an `xarray.Dataset` via `.to_dataset(...)`.
3434

3535
Fits a line `y ~= a*x + b` by minimising the mean squared error, with the
3636
**entire training loop expressed as a single recursive CTE** — no Python
37-
iteration. Two pieces:
38-
39-
- **`grad` compiles the update rule.** `xql.differentiate_sql(loss, "a", cols)`
40-
turns the per-row loss into its symbolic derivative *as SQL text* — the
41-
autograd engine as a calculus compiler.
42-
- **A recursive CTE is the optimiser.** `params(step, a, b)` starts at one row
43-
and each recursion appends the next generation, descending along the gradient
44-
(`AVG` of the compiled rule over the data):
45-
46-
```sql
47-
WITH RECURSIVE params(step, a, b) AS (
48-
SELECT 0, 0.0, 0.0
49-
UNION ALL
50-
SELECT params.step + 1, params.a - lr*AVG(da), params.b - lr*AVG(db)
51-
FROM params CROSS JOIN d WHERE params.step < STEPS
52-
GROUP BY params.step, params.a, params.b)
53-
SELECT * FROM params ORDER BY step
54-
```
37+
iteration and no precompiled update rule. `grad(...)` lives *inside* the
38+
recursion: `params(step, a, b)` starts at one row and each recursion appends the
39+
next generation, descending along the gradient that `grad` computes from the
40+
loss formula directly (`AVG(grad(loss, a))` is the relational `d/da (Σ loss) /
41+
N` — differentiation through the aggregate is just linearity):
42+
43+
```sql
44+
WITH RECURSIVE params(step, a, b) AS (
45+
SELECT 0, 0.0, 0.0
46+
UNION ALL
47+
SELECT params.step + 1,
48+
params.a - lr*AVG(grad(loss, a)),
49+
params.b - lr*AVG(grad(loss, b))
50+
FROM params CROSS JOIN d WHERE params.step < STEPS
51+
GROUP BY params.step, params.a, params.b)
52+
SELECT * FROM params ORDER BY step
53+
```
5554

5655
So gradient, update, and iteration are all declarative SQL; the trajectory is
5756
the rows of one query. The fit matches numpy's least-squares solution.
5857
Self-contained (no network).
5958

60-
(Why differentiate to text instead of `grad(...)` inside the recursion? `grad`
61-
needs the Substrait round-trip, and Substrait has no recursion — so a `grad`
62-
marker can't live inside a recursive CTE. Differentiating once to plain SQL
63-
sidesteps that.)
64-
59+
`grad` works inside the recursive CTE because it is differentiated as a SQL
60+
source-to-source rewrite *before* the query is planned — no Substrait round-trip,
61+
so no plan-shape restrictions. (If you instead want the derivative as a string to
62+
embed yourself, `xql.differentiate_sql(loss, "a", cols)` compiles a single
63+
expression to SQL text.)

benchmarks/grad_descent.py

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,31 @@
1212
"""Gradient descent as a single declarative SQL query.
1313
1414
Fits a line ``y ~= a*x + b`` by minimising the mean squared error — with the
15-
**entire training loop expressed as one recursive CTE**, no Python iteration.
16-
17-
Two pieces:
18-
19-
1. **grad compiles the update rule.** ``differentiate_sql`` turns the per-row
20-
loss into the symbolic derivative *as SQL text* — the autograd engine acting
21-
as a calculus compiler:
22-
23-
da = differentiate_sql("(y-(a*x+b))^2", "a") # -> "-2*((a*x+b)-y)*x", etc.
24-
25-
2. **A recursive CTE is the optimiser.** ``params(step, a, b)`` starts at one
26-
row and each recursion appends the next generation, descending along the
27-
gradient (``AVG`` of the compiled rule over the data):
28-
29-
params.a - lr * AVG(da)
30-
31-
So the whole loop — gradient, update, and iteration — is declarative SQL;
32-
the optimisation trajectory is the rows of one query.
33-
34-
Why two pieces instead of ``grad(...)`` directly inside the recursion? ``grad``
35-
needs the Substrait round-trip, and Substrait has no recursion — so ``grad``
36-
can't live inside a recursive CTE (tracked in #194 / a follow-up). Differentiating
37-
once to plain SQL sidesteps that: the recursive query contains no ``grad`` marker.
15+
**entire training loop expressed as one recursive CTE**, no Python iteration and
16+
no precompiled update rule. ``grad(...)`` lives *inside* the recursion:
17+
18+
WITH RECURSIVE params(step, a, b) AS (
19+
SELECT 0, 0.0, 0.0
20+
UNION ALL
21+
SELECT params.step + 1,
22+
params.a - lr * AVG(grad(loss, a)),
23+
params.b - lr * AVG(grad(loss, b))
24+
FROM params CROSS JOIN d
25+
WHERE params.step < STEPS
26+
GROUP BY params.step, params.a, params.b)
27+
SELECT step, a, b FROM params ORDER BY step
28+
29+
Each recursion appends the next generation, descending along the gradient that
30+
``grad`` computes from the loss formula directly. ``AVG(grad(loss, a))`` is the
31+
relational ``d/da (Σ loss) / N`` — differentiation through the aggregate is just
32+
linearity. So gradient, update, and iteration are all one declarative query; the
33+
optimisation trajectory is the rows of that query.
34+
35+
``grad`` is differentiated as a SQL source-to-source rewrite *before* the query
36+
is planned, so the marker works inside the recursive CTE (and any other query
37+
shape) with no Substrait round-trip. The loss is written once, as ordinary SQL,
38+
and the engine differentiates it symbolically — the relational equivalent of
39+
``jax.vmap(jax.grad(f))``, since each row is an independent evaluation point.
3840
3941
Run standalone:
4042
@@ -48,10 +50,10 @@
4850

4951
import xarray_sql as xql
5052

51-
# Per-row loss r^2 with residual r = y - (a*x + b), over columns a, b, x, y.
53+
# Per-row loss r^2 with residual r = y - (a*x + b). The columns a, b come from
54+
# the recursive `params` relation; x, y come from the data table `d`.
5255
RESIDUAL = "(y - (a * x + b))"
5356
LOSS = f"{RESIDUAL} * {RESIDUAL}"
54-
COLUMNS = ["a", "b", "x", "y"]
5557
LR = 0.4
5658
STEPS = 200
5759

@@ -72,22 +74,17 @@ def main() -> None:
7274
chunks={"i": n},
7375
)
7476

75-
# grad compiles the per-row update rule to SQL, once.
76-
da = xql.differentiate_sql(LOSS, "a", COLUMNS)
77-
db = xql.differentiate_sql(LOSS, "b", COLUMNS)
78-
print(f"d(loss)/da = {da}")
79-
print(f"d(loss)/db = {db}\n")
80-
8177
# The entire training loop is one declarative recursive query: each step
82-
# appends the next generation, descending along the SQL-computed gradient.
78+
# appends the next generation, descending along the gradient that grad()
79+
# computes from the loss — differentiated inside the recursion itself.
8380
trajectory = ctx.sql(
8481
f"""
8582
WITH RECURSIVE params(step, a, b) AS (
8683
SELECT 0 AS step, CAST(0.0 AS DOUBLE) AS a, CAST(0.0 AS DOUBLE) AS b
8784
UNION ALL
88-
SELECT params.step + 1 AS step,
89-
params.a - {LR} * AVG({da}) AS a,
90-
params.b - {LR} * AVG({db}) AS b
85+
SELECT params.step + 1 AS step,
86+
params.a - {LR} * AVG(grad({LOSS}, a)) AS a,
87+
params.b - {LR} * AVG(grad({LOSS}, b)) AS b
9188
FROM params CROSS JOIN d
9289
WHERE params.step < {STEPS}
9390
GROUP BY params.step, params.a, params.b
@@ -107,7 +104,8 @@ def main() -> None:
107104
print(f"least-squares (numpy): a={a_ols:.4f} b={b_ols:.4f}")
108105
assert abs(a - a_ols) < 1e-2 and abs(b - b_ols) < 1e-2
109106
print(
110-
"\nOK: a single recursive-CTE query fit the line to the OLS solution."
107+
"\nOK: a single recursive-CTE query with grad() inside fit the line "
108+
"to the OLS solution."
111109
)
112110

113111

0 commit comments

Comments
 (0)