Skip to content

Add chunked GNO forward pass - #746

Open
abhs21 wants to merge 5 commits into
neuraloperator:mainfrom
abhs21:codex/gno-chunked-forward-issue-726
Open

Add chunked GNO forward pass#746
abhs21 wants to merge 5 commits into
neuraloperator:mainfrom
abhs21:codex/gno-chunked-forward-issue-726

Conversation

@abhs21

@abhs21 abhs21 commented Sep 4, 2026

Copy link
Copy Markdown

Adds chunked_forward to GNOBlock and IntegralTransform for incremental sum and mean aggregation.

Feature gathering happens inside each chunk for both unbatched and batched inputs. The earlier implementation gathered the entire unbatched feature array before the loop, which failed when the edge count exceeded chunk_size.

Validation:

  • Google Colab, Tesla T4 (compute capability 7.5), PyTorch 2.11.0+cu128, CUDA 12.8, zencfg 0.3.0.
  • 72 GNO tests passed: 54 CUDA cases and 18 CPU cases. The chunked comparisons now run on CPU and, when available, CUDA. They compare outputs and gradients for unbatched, single-batch, and multi-batch inputs across three transforms and both reductions.
  • The unbatched regression fails on the earlier implementation.
  • Previous CPU-only validation: 54 GNO tests passed with PyTorch 2.14.0+cpu.
  • Optional Open3D and torch-scatter paths were not exercised.

A synthetic dense-graph benchmark on the T4 measured:

Mode Peak additional allocated memory (MiB) Median elapsed time (ms)
Full inference 172.008 88.528
Chunked inference (4,096 edges/chunk) 8.665 17.246
Full forward + backward 356.350 266.424
Chunked forward + backward 314.104 69.172

The workload has 512 input points, 512 queries, 262,144 edges, 16 feature channels, hidden widths [64, 64], nonlinear transform, mean reduction, and no positional embedding. One warmup precedes three measured repeats. Memory is peak torch.cuda.memory_allocated above the pre-call baseline; timing synchronizes CUDA. Backward measures parameter gradients, not coordinate/input gradients.

These are measurements for one workload, not general memory or throughput guarantees. Neighbor search still constructs the full graph, and autograd retains activations across chunks during training. Measurements used implementation commit d9c5060; commit 3f7516c adds only CUDA test coverage.

Benchmark reproducer

Run in the PR checkout with CUDA PyTorch and the project dependencies installed (zencfg==0.3.0 for this environment).

import gc, json, statistics, time, sys, subprocess
from pathlib import Path
ROOT = Path.cwd()
HEAD = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip()
import torch
sys.path.insert(0, str(ROOT))
from neuralop.layers.gno_block import GNOBlock
torch.manual_seed(726)
block = GNOBlock(in_channels=16, out_channels=16, coord_dim=2,
                 pos_embedding_type=None, radius=10.0, reduction="mean",
                 transform_type="nonlinear", channel_mlp_layers=[64, 64],
                 use_torch_scatter_reduce=False,
                 use_open3d_neighbor_search=False).cuda()
y = torch.rand(512, 2, device="cuda")
x = torch.rand(512, 2, device="cuda")
f = torch.rand(512, 16, device="cuda")
def measure(chunk_size, training):
    times, peaks = [], []
    for repeat in range(4):
        block.zero_grad(set_to_none=True)
        gc.collect()
        torch.cuda.empty_cache()
        torch.cuda.synchronize()
        baseline = torch.cuda.memory_allocated()
        torch.cuda.reset_peak_memory_stats()
        start = time.perf_counter()
        with torch.set_grad_enabled(training):
            output = block(y, x, f_y=f) if chunk_size is None else block.chunked_forward(y, x, f_y=f, chunk_size=chunk_size)
            if training:
                output.square().mean().backward()
        torch.cuda.synchronize()
        if repeat > 0:
            times.append((time.perf_counter() - start)*1000)
            peaks.append((torch.cuda.max_memory_allocated() - baseline)/2**20)
        del output
    return {"median_ms": statistics.median(times), "peak_extra_allocated_mib": max(peaks)}
memory_report = {
    "source_commit": HEAD,
    "torch": torch.__version__, "cuda": torch.version.cuda,
    "gpu": torch.cuda.get_device_name(0),
    "shape": {"input_points":512, "query_points":512, "edges":512*512, "features":16, "hidden":[64,64]},
    "repeats":3, "warmup":1,
    "note":"Synthetic dense graph; incremental torch-allocated memory includes graph construction. Training includes parameter gradients, not input gradients. Optional Open3D/torch-scatter disabled.",
    "results": {}
}
for training in (False, True):
    for chunk_size in (None, 4096):
        key = ("training" if training else "inference") + ("_full" if chunk_size is None else "_chunk4096")
        memory_report["results"][key] = measure(chunk_size, training)
print(json.dumps(memory_report, indent=2))
Path("gno_memory_benchmark.json").write_text(json.dumps(memory_report, indent=2))

Related to #726.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant