Skip to content

WIP: GraphCast_small in SQL, with DeepMind's published weights - #270

Draft
alxmrs wants to merge 30 commits into
mainfrom
graphcast-weights
Draft

alxmrs wants to merge 30 commits into
mainfrom
graphcast-weights

Conversation

@alxmrs

@alxmrs alxmrs commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Stacked on #269. This replaces the miniature 03_graphcast.py with GraphCast_small as DeepMind publishes it:

  • a 1° grid with 13 pressure levels;
  • a mesh refined 5 times;
  • 16 message-passing rounds;
  • 36M weights.

GraphCast is DeepMind's graph neural network for weather forecasting. Here it's written as SQL views over tables registered through xarray-sql, one view per stage.

Status: work in progress. As stored tables, the encoder and one processor round have run end to end on einfold and match DeepMind's model. As views, it doesn't run past the encoder on any engine yet, for the upstream reasons below.

What's here

  • _graphcast.py: everything on DeepMind's side.
    • It fetches the checkpoint, the 1° example from ERA5 (ECMWF's reanalysis of the atmosphere) and the normalization statistics from gs://dm_graphcast, cached in ~/.cache/xarray-sql/graphcast.
    • It builds the three graphs with DeepMind's own code: grid→mesh with 101,892 edges, mesh with 81,900, and mesh→grid with 195,480. It exports them as Datasets.
    • It turns the checkpoint into long-form tables: weights(net, f, g, w), biases, norms and nets(net, name).
    • deepmind_stages() and deepmind_forecast() run DeepMind's model in float32.
    • mirror() is a plain JAX transcription of the model, checked against DeepMind's. It can stop after any processor round, so the SQL can be checked one round at a time.
  • 03_graphcast.py: the model in SQL.
    • Every network is the same mlp(): a matrix product, bias plus swish (x · sigmoid(x)), a second matrix product and bias, then a layer norm through OVER (PARTITION BY id) windows.
    • Each stage is a CREATE VIEW of the stages before it.
    • The checks and the forecast are plain queries of those views.
    • Inputs are normalized and stacked into DeepMind's 183 channels in SQL. The mesh nodes' zero inputs are absent rows.
    • receptive_field() and program(..., regional=True) write an exact regional forecast: the same stages, each limited by semi-joins to what the region needs. It hasn't run yet.
    • --sizes is the number of processor rounds. At 16, the forecast is checked against DeepMind's.
  • Hosts take a memory limit (--memory, in GB), and spill to ~/.cache/xarray-sql/spill.

Measured

Measured on an Intel i7-8700 with einfold 0.1.0rc2, DataFusion 55.0.0, DuckDB 1.5.6 and ddxdb 0.4.6. Details are in #285 and xqlsystems/einfold#36.

Next

  1. Run the views once DataFusion computes each shared read once (DataFusion plans a CTE once per reference, so ML queries grow exponentially with depth #267).
  2. Run the full 16-round forecast and compare it with deepmind_forecast().
  3. Run the exact regional forecast, then an approximate one (ORDER BY distance LIMIT k), and plot its error.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h

@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@alxmrs
alxmrs force-pushed the graphcast-weights branch 8 times, most recently from b821fa1 to 644ead4 Compare October 21, 2026 00:23
@alxmrs
alxmrs force-pushed the graphcast-weights branch from 644ead4 to 4f29d2f Compare October 21, 2026 09:17
@alxmrs
alxmrs force-pushed the graphcast-weights branch 2 times, most recently from bdce766 to 8f358fd Compare October 25, 2026 03:25
alxmrs added a commit that referenced this pull request Oct 25, 2026
GraphCast takes about 6 minutes at its default 2 rounds and builds on
WeatherBench 2 data, so it lands with the work on its published weights
(#270). This PR keeps the attention and nanoGPT demos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
@alxmrs
alxmrs force-pushed the graphcast-weights branch from 8f358fd to 785c32b Compare October 25, 2026 16:51
@alxmrs
alxmrs force-pushed the graphcast-weights branch from 785c32b to 1721481 Compare October 25, 2026 18:44
@alxmrs
alxmrs force-pushed the graphcast-weights branch from 1721481 to 12e33f5 Compare October 25, 2026 19:24
@alxmrs
alxmrs force-pushed the ml-demos branch 2 times, most recently from 536b005 to 968b41e Compare October 25, 2026 19:25
@alxmrs
alxmrs force-pushed the graphcast-weights branch 3 times, most recently from b0fe9af to e05fae2 Compare October 25, 2026 20:56
@alxmrs alxmrs added the ml Gradients in SQL, the ml extra and its demos label Oct 25, 2026
alxmrs and others added 10 commits October 25, 2026 19:10
ddxdb 0.3.3 keeps each gradient program between calls (xqlsystems/ddx#164),
plans each CTE an objective reads twice on its own (xqlsystems/ddx#168),
and differentiates attention's masked product as jax.grad does
(xqlsystems/ddx#162). Alternating runs with 0.3.2 on DataFusion:

- GPT training step, SGD: 2.8 s -> 1.2 s (first step 2.0 s)
- GPT training step, AdamW: 3.2 s -> 1.55 s
- attention gradient at 512 tokens: 2.8 s -> 1.1 s (window),
  2.7 s -> 1.3 s (GROUP BY)

Every check against JAX still passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
ddxdb 0.4.0 exchanges Substrait with datafusion 55.0.0, the version
xarray-sql now pins. Its gradients, programs and API are unchanged from
0.3.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
On DataFusion 55 with ddxdb 0.4.0 and xarray-sql's in-memory tables, the
GPT's forward loss takes 3.0 s (9.4 s), an SGD step 0.75 s (1.2 s) and
an AdamW step 0.85 s (1.55 s). Attention's forward pass at 512 tokens
takes 0.28 s and 0.37 s, and its gradients 0.92 s and 0.93 s. Every
result still matches JAX.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
The DuckDB host sends statements that call grad, vjp or jvp to
ddxdb.duckdb.sql_all, as xarray-sql's Context does on DataFusion. A
gradient ddx or DuckDB's Substrait producer refuses is recorded with the
reason (Report.attempt), and a refused training step ends that host's
run, since later steps start from its weights.

Measured with ddx's development branch (after ddxdb 0.4.1): attention's
GROUP BY spelling differentiates on DuckDB and matches jax.grad; the
window spelling and the GPT are refused (xqlsystems/ddx#195). The
README records the timings and why DuckDB's gradient is slower: its
join order over Arrow scans in ddx's two big contractions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
--layers, --embed, --heads and --context set nanoGPT's n_layer, n_embd,
n_head and block_size; the defaults are the model as it was. The README
records a profile on DataFusion: the loss's time is planning (a reused
physical plan runs in 1.8 ms), a second layer multiplies the plan by
about 100 and runs out of memory, and JAX's times are the target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
GraphCast takes about 6 minutes at its default 2 rounds and builds on
WeatherBench 2 data, so it lands with the work on its published weights
(#270). This PR keeps the attention and nanoGPT demos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
block() was one long template. It is now the composition nanoGPT's Block
is, of CausalSelfAttention and MLP, with the residual connections as their
own function. The SQL is the same, but for two table aliases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
tests/test_ml_demos.py runs every demo at its smallest size, each checked
against JAX, and a new ml demos CI job installs JAX to run it.
benchmarks/ml/CONTRIBUTING.md says what a demo is made of, how to write
its SQL, and what to do when ddx or an engine can't run it. The
performance work is tracked in #285.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
ddxdb 0.4.2 has the DuckDB backend, ddxdb.duckdb, which the DuckDB host
imports; 0.4.1 didn't. The demos' numbers are re-recorded on it. Every
check against JAX passes. On DuckDB, a loss may now take a gradient with
respect to a table that a view doesn't read. A view whose CTE computes a
window and is read twice is still refused (xqlsystems/ddx#204).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
alxmrs and others added 11 commits October 26, 2026 18:16
…uckDB

- A demo names the refusals it expects, by host and the start of the
  error; Report.finish exits with an error for any other, so CI notices a
  gradient a new ddxdb refuses.
- CI runs demo 2 with AdamW too.
- tests/test_ad.py: vjp and jvp through Context.sql, against their closed
  forms; the refusal test reaches a GROUP BY key that depends on w.
- jax and ddxdb[duckdb] join the dev group, so the lock pins JAX and puts
  DuckDB at the 1.5.6 ddxdb declares; CI's ml demos job no longer installs
  JAX on its own, and the unit test job leaves the demos to it.
- Docs: run a ddx checkout with .venv/bin/python, since uv run ignores
  --no-sync for a script with inline metadata; a demo's dependency goes in
  the dev group too; the lazy read's cost and the JAX comparison, fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
Context.sql sends a query that calls grad, vjp or jvp to ddx, and runs any
other through main's path, which forgets a replaced table's Dataset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
ddxdb 0.4.3 differentiates a view whose CTE computes a window on DuckDB
(xqlsystems/ddx#204), so the demos no longer expect DuckDB to refuse
attention's window spelling or the GPT's training step. Record the GPT
on DuckDB: its weights match JAX's, its loss takes 0.062 s against
DataFusion's 2.7 s, and an SGD step takes 0.87 s against 0.41 s, most of
it DuckDB planning ddx's 77 steps afresh and the adapter's reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
DataFusion plans a CTE again at every place that reads it, so a model
whose CTEs read each other more than once has a plan that grows
exponentially with its depth (#267). The 1-layer loss took 2.7 s at the
demo's size and 108 s at nanoGPT's CPU size, and the 2-layer loss ran out
of memory while planning.

- The GPT's SQL builders return their CTEs as (name, sql) pairs, and
  every statement starts with the model's and the loss's CTEs, through
  _hosts.with_query.
- Hosts gain query_with(ctes, select) and operators_with. DataFusion's
  runs each CTE on its own into an in-memory table that later CTEs read,
  under TODO(apache/datafusion#26237). It uses collect_partitioned and
  register_record_batches rather than DataFrame.cache(), which einfold
  can't yet plan a join of (xqlsystems/einfold#93).
- The model is no longer the view logits. Creating a DataFusion view
  writes its CTEs out, so the 3-layer view didn't fit in 8 GB; ddx, given
  the CTEs inline, computes each shared one once.

apache/datafusion#24780's streaming fan-out example doesn't fit these
queries: by its own notes it can deadlock when both sides of a hash join
read the shared plan, and doesn't nest. DataFusion 55 parses
`AS MATERIALIZED` only in its PostgreSQL dialect, and ignores it.

With ddxdb 0.4.4, at batch 4, on an Intel i7-8700: the 2-layer loss takes
0.07 s on DataFusion, and a 6-layer SGD step 2.4 s, against 0.30 s and
5.5 s on DuckDB; every step matches JAX.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
…reader

ddxdb 0.4.5 releases ddx's fixes for attention's backward (xqlsystems/ddx#220,
#225), drops intermediate tables once nothing reads them (#223), and halves
DuckDB's per-call overhead (#224). The GPT demo's steps match JAX on it, on
DataFusion and DuckDB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
einfold, a query optimizer for DataFusion that runs tensor computations
in SQL with array algorithms, released its first candidate. The demos
gain an einfold host: xarray-sql's Context with einfold as its query
planner. Demo 1 now checks that einfold runs both spellings of
attention, and the Nadaraya-Watson control, as one fused pipeline:
FlashAttention's tiled plan, found from the SQL. The README gives
einfold's numbers.

einfold has no Windows wheel yet (xqlsystems/einfold#118), so the dev
group skips it on Windows rather than build it from source.

ddxdb 0.4.6 differentiates through UNION ALL and computes a step read
once inside its reader, which takes the GPT's program from 77 steps
to 64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
Context.sql sent a query to ddx when "grad(" appeared anywhere in its
text, so a parameterized query with the word in a string literal raised
TypeError. calls_ad now ignores string literals and comments.

jaxlib publishes no wheels for Intel macOS, so the dev group installs
JAX everywhere else, and the demos' test skips without it. The demos'
README says what the Nadaraya-Watson estimator is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
einfold 0.1.0rc2 allocates large blocks with the system's allocator, so
fused attention at 32,768 tokens peaks 0.1 GB lower than on rc1
(xqlsystems/einfold#119). The ml demos job installs the test extra too,
as review asked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
Each piece of the model took the names of its input and output tables
and returned the CTEs it added, as (name, sql) pairs: a shape the
DataFusion workaround for shared CTEs needed, which made the model hard
to read. Now each piece takes relations and returns one, a SELECT that
reads its inputs by name, so a block reads as nanoGPT's Block.forward:

    x = residual(x, attention(layer_norm(x, "ln_1", layer), layer))
    return residual(x, mlp(layer_norm(x, "ln_2", layer), layer))

_relation.py lists every relation a loss reads, in order, as the CTEs of
one query, and names each one, never as a table it would hide. The SQL
is the same, CTE for CTE, and the demo matches JAX as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
The model is a view again, built by functions that each return a few of
its CTEs as text, and the loss is a query that reads it. The DataFusion
host no longer runs each CTE on its own, and _relation.py is gone: the
demo shows the SQL a reader would write, and the work to make it fast
on DataFusion belongs upstream. ddx computes each CTE once on its own,
so training steps are as fast as before; DataFusion's forward loss takes
2.8 s rather than 0.04 s, and a model of more than one layer doesn't
plan on DataFusion until it computes a shared CTE once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
Each stage of GraphCast_small is now a view of the stages before it,
and the checks and the forecast are plain queries of those views. This
drops Host.store and Host.drop, which kept each stage as a table so no
engine computed a CTE twice: the demos write the SQL a reader would, and
making it fast is the engines' work. The hosts keep their memory limit
and spill directory, which change no SQL.

Rebased onto ml-demos at 6debe24: einfold is a host, with the memory
limit like the others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
Base automatically changed from ml-demos to main October 31, 2026 22:21

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ml Gradients in SQL, the ml extra and its demos

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant