This file provides guidance to AI agents when working with code in this repository.
This is a monorepo of four registered Julia packages wired together with a top-level Julia [workspace] (Project.toml). Dependencies flow one way:
GNNGraphs ──► GNNlib ──► GraphNeuralNetworks (Flux frontend)
│ │
└─────────────┴──────► GNNLux (Lux frontend)
- GNNGraphs — graph data structures (
GNNGraph,GNNHeteroGraph,TemporalSnapshotsGNNGraph,DataStore) and graph ops (query/transform/generate/sampling/convert). No deep-learning framework dependency. - GNNlib — framework-agnostic message-passing engine (
propagate,apply_edges,aggregate_neighbors, gather/scatter) and the functional (stateless) implementation of every layer's forward pass (e.g.gcn_conv,gat_conv). Not meant for direct end-user use; re-exported by the frontends. - GraphNeuralNetworks — Flux frontend. Stateful mutable-struct layers (
GCNConv,GATConv, ...) whose forward pass calls into GNNlib. This is the package that shares the repo name. - GNNLux — Lux frontend. Stateless
@concrete/AbstractLuxLayerversions of the same layers, also calling into GNNlib.
The frontends @reexport using GNNGraphs and GNNlib, so users only install one frontend.
Each package mirrors the same src/layers/ split: basic.jl, conv.jl, pool.jl, temporalconv.jl (plus heteroconv.jl in the Flux frontend). A given layer therefore appears in up to three places — its math in GNNlib/src/layers/conv.jl, its Flux struct in GraphNeuralNetworks/src/layers/conv.jl, its Lux struct in GNNLux/src/layers/conv.jl.
Julia ≥ 1.10 is required (some tests, e.g. Mooncake, need ≥ 1.12). Thanks to the workspace, activating any package makes the sibling packages available in dev mode automatically — no manual Pkg.dev needed for local development.
# Run one package's full CPU test suite
julia --project=GraphNeuralNetworks -e 'using Pkg; Pkg.test("GraphNeuralNetworks")'
julia --project=GNNGraphs -e 'using Pkg; Pkg.test("GNNGraphs")'
# Run only the :gpu-tagged items on CUDA (works locally on a CUDA machine, not
# just in CI). The test harness auto-installs the backend (CUDA + cuDNN) and sets
# allowscalar(false); swap in GNN_TEST_AMDGPU / GNN_TEST_Metal for other backends.
GNN_TEST_CPU=false GNN_TEST_CUDA=true julia --project=GNNGraphs -e 'using Pkg; Pkg.test("GNNGraphs")'
# Build docs for one package
julia --project=GNNGraphs/docs GNNGraphs/docs/make.jl
# Format code (SciML style, configured in .JuliaFormatter.toml)
julia -e 'using JuliaFormatter; format(".")'Test files define independent @testitem "..." setup=[TestModule] begin ... end blocks. runtests.jl in each package just calls @run_package_tests with a tag filter. Shared fixtures and exports live in each package's test/test_module.jl (TestModule).
- Run a single test item: use the VS Code Julia extension's test-item UI, or filter in
runtests.jl. Test items are self-contained, so you can also copy one into a REPL afterinclude("test/test_module.jl"). - Shape +
test_gradientsdo NOT verify forward-pass correctness. The sharedtest_gradientshelper only checks that AD matches finite differences of whatever function is coded, and@test size(...) == ...only checks dimensions. A layer whose forward pass computes the wrong formula passes both (its wrong gradient is still internally consistent). - GPU tests are tagged
:gpu; untagged items are CPU tests. Backend selection is via env vars read inruntests.jlandtest_module.jl:GNN_TEST_CPU(default"true"),GNN_TEST_CUDA,GNN_TEST_AMDGPU,GNN_TEST_Metal. CPU CI runs untagged items; GPU CI (Buildkite,.buildkite/pipeline.yml) setsGNN_TEST_CPU=falseand a backend var totrue.test_module.jlPkg.adds the backend on demand, so it need not be intest/Project.toml. - Exercise a
:gpuitem in the REPL: inside a@testitem,dev = gpu_device(force=true)and helpers likeAbstractGPUDevicecome from the package's test-module setup (GraphsTestModulein GNNGraphs). To reproduce the same environment interactively, load the backend the way the harness does — for CUDA that meansusing CUDA, cuDNN(cuDNN is required; without itgpu_device()reports "no functional GPU backend" and silently falls back to the CPU) — then setCUDA.allowscalar(false)so any accidental scalar indexing errors instead of running slowly (this is what catches most GPU regressions, e.g. graph ops that materialize a CPU mask).
Per GraphNeuralNetworks/docs/src/dev.md, a layer must be added in all frontends:
- Functional forward pass in GNNlib (
src/layers/conv.jl, exported lowercase e.g.my_conv). - Stateful struct in GraphNeuralNetworks (Flux), calling the GNNlib function.
- Stateless struct in GNNLux (Lux).
- Add it to the table in the docs
api/conv.md.
Recommended workflow: build a self-contained Flux layer first with tests, then move the forward-pass body into GNNlib, then add the Lux version. The @structdef macro from AutoStructs.jl can generate the struct + constructor together.
Layers typically implement their forward pass via propagate(fmsg, g, aggr; xi, xj, e) — see the extensive docstring at the top of GNNlib/src/msgpass.jl. xi/xj are target/source node features materialized onto edges; e is edge features.
- GPU support is provided through package extensions (
ext/, e.g.GNNGraphsCUDAExt,GNNlibCUDAExt/AMDGPUExt/MetalExt) gated on weakdeps — don't add a hard GPU dependency. - Mooncake and SimpleWeightedGraphs support are also extensions; keep them optional.
- Versioning: each PR should bump the
versionin theProject.tomlof every affected package per semver, using a-DEVsuffix for unreleased changes (e.g.1.18.0-DEV), and update inter-package[compat]bounds when a frontend needs newer GNNGraphs/GNNlib. - Changelog: there is a single repo-wide
CHANGELOG.mdat the root, organized as one timeline (most recent first), with each release headed## <Package>.jl <version> — <date>. Any user-facing change (new feature, bug fix, deprecation, or compat relaxation) must add an entry under the relevant package's latest (orUnreleased) heading, in Keep a Changelog style, linking the PR ([#NNN]reference at the bottom of the file). Skip purely internal churn (tests, CI, docs, refactors). When tagging a release, turn the package'sUnreleased/-DEVheading into a dated<version> — YYYY-MM-DDheading placed at the top of the timeline. - Follow the
[workspace]dev model; do not rewire packages to local paths outside the repo.