Repository navigation
Run supported GraphTransformerEncoder configs on packed tokens - #811
kmontemayor2-sc wants to merge 57 commits into
Conversation
…eroData GraphTransformerEncoder.forward built a new HeteroData from the projected features and called heterodata_to_graph_transformer_input, which converts it with to_homogeneous. Dynamo cannot trace either step, so torch.compile of the whole encoder failed. Add tensors_to_graph_transformer_input, which takes x/edge_index/edge_attr dicts and the graph-level relative-encoding matrices and concatenates them in the same node and edge type order to_homogeneous uses. heterodata_to_graph_transformer_input is now a thin wrapper over it, and the encoder calls it directly. Node features are gathered with a masked gather instead of bool-mask indexing, which drops a host sync. Outputs are unchanged: torch.equal against the previous implementation over 432 transform configurations (k-hop/PPR, in/out, hop order, relation edges, explicit anchors, multiple node and edge types). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the per-seed-role PPR relation filter (#799). The encoder no longer builds a projected HeteroData, so the expansion flags and typed-PPR channel masks now reach the transform as explicit tensors_to_graph_transformer_input arguments (ppr_is_expanded_dict, ppr_relation_channel_masks); heterodata_to_graph_transformer_input reads them from the batch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A missing-attribute lookup on HeteroData runs a regex, and torch.compile guards on the regex cache, which broke the compiled-forward test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Matches the HeteroData wrapper, which already skips stores that carry only edge_attr instead of raising AttributeError. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- On CUDA, khop / anchor_only configs with no attention bias build their sequences with a fixed-shape layout (gigl/transforms/static_khop.py) and run the layers on one [tokens, dim] buffer with varlen attention (gigl/nn/packed_attention.py), so no FLOPs go to padding. - The encoder picks the path per forward: packed when the config supports it, on CUDA, and not training with attention dropout; otherwise the existing padded path runs unchanged. Both give the same embeddings and gradients up to float error. - GraphTransformerEncoderLayer.forward_packed is public; the packed path costs two host reads per batch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Require a head dim divisible by 8 and hop_distance <= 2 for the packed path - Accept a string device in GraphTransformerEncoder.forward - Make varlen_attention, padded_packed_attention, build_neighbor_table and khop_sequence_layout private; make pack_sequences generic over the grid - Trim the encoder notes and CHANGELOG entry to the full condition set - Tests: fold the CUDA predicate check into the CPU one, run the compile test on CUDA, parity over hop_distance 1 and 2, drop covered static_khop cases Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HeteroData.to moves the batch in place, so the CPU encoder was fed CUDA data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…parse layout Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
/all_test |
GiGL Automation@ 18:50:26UTC : 🔄 @ 20:57:10UTC : ✅ Workflow completed successfully. |
GiGL Automation@ 18:50:26UTC : 🔄 @ 19:00:13UTC : ✅ Workflow completed successfully. |
GiGL Automation@ 18:50:26UTC : 🔄 @ 20:23:03UTC : ✅ Workflow completed successfully. |
GiGL Automation@ 18:50:27UTC : 🔄 @ 19:00:59UTC : ✅ Workflow completed successfully. |
GiGL Automation@ 18:50:27UTC : 🔄 @ 20:31:17UTC : ✅ Workflow completed successfully. |
GiGL Automation@ 18:50:28UTC : 🔄 @ 18:52:31UTC : ✅ Workflow completed successfully. |
… blockers, layout budget - Clamp the varlen kernel's length bounds to the token counts so a training step on a batch with fewer tokens than max_seq_len no longer fails in backward. - List why a config cannot run packed (_packed_backend_blockers) and log it at construction; attention_dropout_rate > 0 is a blocker so training and eval take the same path. - Fall back to the padded path for a batch whose k-hop candidate set exceeds a budget. - The k-hop layout accepts int32 edge indices and anchors; the homogeneous edge index helper places the result on the device and keeps the dtype. The shared helpers are public, and PackedSequences/pack_sequences live next to the layout. - Add the hop embedding directly on the packed path and read the batch's relative-encoding attributes only on the padded path. - once_differentiable backward, grad-mode aware needs_grad, and validation of max_seq_len and hop_distance. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The memory-efficient attention kernels were called as aten ops from an autograd.Function. Inductor applies layout rules to those ops that need concrete strides, and it fails on the [1, tokens, ...] views when the token count is only known at run time (whole-encoder torch.compile). Wrap forward and backward as torch.library custom ops, gigl::varlen_attention_forward and gigl::varlen_attention_backward, with fake implementations, so Inductor treats them as opaque. Eager numerics are unchanged.
…put projection _encode_packed contains graph breaks (data-dependent sizes), and a graph break inside a call leaves the bound method on the interpreter stack. torch.compile guards on that fresh bound-method object, so nesting the call in _project_output(...) recompiles a whole-encoder compile on every batch. The result is assigned to a local first; add a comment saying so. Add tests that a compiled encoder reuses its graphs across batches (CPU, eager backend) and that the whole encoder compiles with Inductor on CUDA and matches eager.
The packed backend runs without attention dropout, so the forward op returns only (output, logsumexp) and the backward kernel builds the dropout-free Philox scalars itself. The Python layer wraps the forward result in VarlenAttentionOutput. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Annotations only, no runtime checking. The registered custom-op kernels and fakes keep plain Tensor types because custom_op infers the op schema from the annotations and rejects jaxtyping types; their docstrings carry the shapes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ecting Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A batch whose k-hop candidate set is too large for the packed layout runs on the padded path. The count and the warning live in a torch.compiler.disable helper so the counter does not become a Dynamo guard. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…_layout and the logger Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…distance; update CHANGELOG A batch that falls back to the padded path needs the hop_distance attribute. Say so in the Args docstring, raise an error naming AddHeteroHopDistanceEncoding instead of the generic missing-attribute error, and cover the fallback half in a test. Rewrite the CHANGELOG entry from the current supported configs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… the neighbour table Index the neighbour table with the frontier directly, sort on (node << hop_bits) | hop, scatter and gather that one key, and decode with shift and mask. khop_layout now returns (layout, lengths, hop_bits). The width is read and the budget (candidates plus the (num_nodes + 1) x width table) is checked before the table is allocated. Layouts are bit-identical to the previous implementation on 1961 random cases. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…, inline the hop embedding check _encode_packed returns the projected embeddings, so forward keeps no half-evaluated call across the Dynamo graph break and the long comment goes. The packed FFN works on [tokens, dim] directly. The one-caller hop-embedding check moves into the constructor, and _packed_backend_blockers reads the stored attributes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
One to_dense_batch over cat(q, k, v) and a chunk; the first-token case places the queries in a zero buffer and keeps each sequence's first row. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ile and layout tests Coverage is kept: the two matches-padded sweeps, the two CPU gradient tests, the two compile tests and the three layout sweeps become one parametrized test each; the three CUDA training-gradient tests become one; the CUDA varlen forward test also covers the no-grad path. The CUDA layer-only compile test is covered by the whole-encoder Inductor test (it runs a middle layer and the first-token last layer). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ate the shapes Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nd the why Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # tests/unit/transforms/static_khop_test.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| "relation_message_mode to be 'none'." | ||
| ) | ||
| if self.training and self._attention_dropout_rate > 0: | ||
| raise ValueError( |
| token_projection = cast(nn.Module, self._token_input_projection) | ||
| x = x + token_projection(token_hop.unsqueeze(-1).to(x.dtype)) | ||
| for layer_index, encoder_layer in enumerate(self._encoder_layers): | ||
| x = cast(GraphTransformerEncoderLayer, encoder_layer).forward_packed( |
There was a problem hiding this comment.
robot comment: _encode_packed() calls encoder_layer.forward_packed(...), but torch.compile(layer) only compiles the module’s ordinary forward. Custom methods delegate to the original module and execute eagerly. BAGL’s active inference helper replaces every encoder layer with torch.compile(layer) (line 88), so supported CUDA configurations will automatically switch to packed execution while enable_torch_compile silently stops compiling the layers. In an exact-head reproduction, compiled.forward_packed(...) left Dynamo counters empty; compiled(...) produced one captured graph. Route packed inputs through the compiled callable, explicitly wrap forward_packed, or change BAGL to whole-encoder compilation, with a regression covering the actual OptimizedModule wrapper.
…ed backend Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every contracted function shares the code object of Jaxtyping's wrapper, so Dynamo counted each one that started a frame as a recompile of the same code. The launcher now runs that frame eagerly with skip_code, and Jaxtyping's config reads jaxtyping_disable as true while Dynamo traces, so inlined wrappers call the undecorated function. Eager calls are still checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The error and the encoder docstring now show how to compile each layer's forward and forward_packed in place, besides whole-encoder compile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR 3/3 for Packed Attention in GT
Summary
GraphTransformerEncoderpads every sequence tomax_seq_len. When sampled batches fill only a fraction of theslots, most of the attention and feed-forward work goes to padding.
attention). The encoder picks it automatically on CUDA when the configuration supports it. There are no new arguments.
max_seq_len256, 2-hop sampled batches, 512 anchors), the packedbackend is about 4x faster than
origin/mainin both inference and training, and its peak memory is about a third.Outputs match the padded path.
kmonte/gt-static-khop:khop_layout,pack_sequences,PackedSequences) and the packed attention PR (kmonte/gt-packed-attention:packed_attention), which hold thelayout and attention details. This PR is the encoder integration. Next, the neighbour-attention readout PR
(
kmonte/gt-packed-neighbor-readout) stacks on this one.tensors_to_graph_transformer_input, Build GT sequence inputs from tensor dicts instead of an internal HeteroData #809), and supersedes the earlierstacked PRs (length bucketing, layout-based hop distance, packed forward, packed training).
Approach
batch whose layout is over budget falls back to the padded path, and the encoder counts these batches and warns.
the largest node degree and the total token count. It then allocates exactly that many tokens.
GraphTransformerEncoderLayer.forward_packedis the packed version of the layer forward. It shares weights with thepadded
forward, so state dicts are unchanged.alone (
query_is_first_tokenin the attention PR).hop_distancetoken input is derived from the layout (the first-reach hop), so the batchattribute is not needed on the packed path.
packed.
gigl/transforms/graph_transformer.pythat the encoder needs are now public:get_node_type_offsets,concat_tensors,to_homogeneous_edge_indexandHOP_DISTANCE_FEATURE_NAME.When packed is used
The encoder uses the packed backend only when all of these hold. Otherwise it runs the padded path as before.
khop(prioritize_hop_orderis supported).anchor_only.none.hop_distance).hop_distanceis at most 4.attention_dropout_rateis 0.The
hop_distance <= 4limit comes from memory. The packed layout grows as anchors x width^hops, so past four hopsnearly every batch would exceed the budget below. Per batch, the layout is also skipped (the batch runs padded; the
encoder counts these batches and warns on the 1st, 2nd, 4th, 8th, ... one) when
anchors x (1 + width + ... + width^hop_distance) + (nodes + 1) x widthexceeds 2^25 cells, wherewidthis thelargest node degree over all edge types. The second term is the neighbour table, and the check runs before the table is
allocated, so a hub node cannot blow up memory either way.
Why attention dropout is excluded:
stays off until it has been.
Path selection:
paths define the
hop_distancetoken input slightly differently when the batch attribute was built with anotherh_maxor direction. When the attribute is present, the transform'sh_maxmust be at least the encoder'shop_distanceand its direction must matchsampling_direction.hop_distanceembedding on a config that can run packed must have more thanhop_distanceentries; the constructorraises
ValueErrorotherwise.User-facing changes
GraphTransformerEncoderLayer.forward_packedget_node_type_offsets,concat_tensors,to_homogeneous_edge_indexandHOP_DISTANCE_FEATURE_NAMEingigl/transforms/graph_transformer.py, previously private helperskhop_layout,pack_sequencesandPackedSequencescome from the k-hop layout PR, andpacked_attentionfrom thepacked attention PR.
Verification), but the computation is different.
earlier ones bit for bit.
hop_distanceattribute is not read and need not exist. Hop distance is computedfrom the layout as the directed first-reach hop. A batch that falls back to the padded path (layout over budget)
does need the attribute: without it the encoder raises a
ValueErrorthat namesAddHeteroHopDistanceEncoding.Keep the transform in the pipeline unless every batch is sure to fit the budget.
deviceargument offorwardnow accepts a string such as"cuda"as well as atorch.device.GraphTransformerEncoderraisesValueErrorformax_seq_len < 1orhop_distance < 0.Verification
ruffandtyclean.packed_graph_transformer_test: 49 tests (7 CUDA-only skipped).packed_attention_test: 3 (2 skipped).nn/graph_transformer_test: 76.transforms/graph_transformer_test: 46 (1 skipped).static_khop_test: 7._encode_packeddoctest passes.pack_sequencesunit tests moved to the PRs that add that code (the packed attention PR and thek-hop layout PR). This PR's tests cover the encoder.
CUDA-only tests cover the real kernels:
now live in the packed attention PR).
torch.cudasync debug mode. The L4run printed
SYNC_COUNT 2.gt-split-ut-20261023-165324, one L4 (g2-standard-48,cuda:0), about 80 s of test time: 181tests pass, none skipped, so every CUDA test ran.
packed_graph_transformer_test49,packed_attention_test3,nn/graph_transformer_test76,transforms/graph_transformer_test46,static_khop_test7.gt-adv-a-20261022-185022,g2-standard-48,cuda:0, testsran 45 s): the packed test module,
static_khop_testand the other unit tests it imports pass in full, 59 tests, noneskipped, including every CUDA test (the 2-sync assertion, the 1- and 2-anchor training steps, CUDA training gradients
at
hop_distance=3and withprioritize_hop_order, the varlen forward with and without grad, and the Inductorwhole-encoder compile).
origin/maineager):compiled
origin/mainwhole-encoder run shows the same 2.2e-4 against its own eager).Whole-encoder
torch.compiletorch.compile(encoder)on the packed backend had two problems, both fixed in this PR:forwardpassedself._encode_packed(...)inline toself._project_output(...)._encode_packedhas graph breaks (data-dependent sizes), and a graph break inside a call leaves the bound method onDynamo's stack.
torch.compileguards on that fresh object, so the guard never matches the next batch. The outputprojection now runs at the end of
_encode_packed, so nothing is pending across the break (a CPU test checks thegraph count stays flat across batches, with and without a fallback batch).
torch.librarycustom opswith fakes and a hand-written autograd Function, on the belief that Inductor's layout rules for the aten op fail on
the
[1, tokens, ...]views. They do not here: the token count is a backed dynamic dimension, because the layout'shost reads break the graph first. The encoder now calls
torch.ops.aten._efficient_attention_forwarddirectly (atensupplies the autograd formula and the meta function) and asks for the logsumexp only when a gradient is needed. That
removed about 280 lines. The Inductor compile tests, the compile-trace matrix below and every CUDA test pass.
Result on one L4 (job
gt-adv-a-20261022-185022, about 31 min program wall):100 distinct training steps (256 queries plus 512 random negatives), then a replay of the first batches.
compile for training (245.7 vs 256.1 ms/step, about 4% faster, at 215 s vs 93 s one-time compile). All variants
settle.
torch.compiler.disable; it is a benchmarkvariant, not part of the PR.
whole-encoder compile and 1.6e-7 for per-layer compile. Eager in bf16 autocast differs from fp32 eager by 1.25e-2, so
the bf16 gradient differences above are precision noise, not a compile difference.
the pre-split tip, which has the same code as this PR.
Performance
One L4, fp32 with TF32, inference and a full train step (forward + backward).
max_seq_len256, 2-hop sampled batches with fanout 15, 512 anchors, about 68of 256 token slots filled, same weights in every cell.
same hardware, with 2 host syncs and exact parity. The earlier harness run of the unfixed tip read 47.7 ms/batch and
152.5 ms/step.
Inference, ms per batch (peak GiB):
torch.compiletorch.compile(encoder)Train step, ms per step (peak GiB):
torch.compiletorch.compile(encoder)origin/mainand Build GT sequence inputs from tensor dicts instead of an internal HeteroData #809 per-layer compile fails in training with an InductorAssertionErrorin the padded gather(shown as "fails").
origin/main(training fails), 2.4 s onBuild GT sequence inputs from tensor dicts instead of an internal HeteroData #809 (training fails), and 6.2 / 13.4 s on this PR.
origin/main20 graphs, 16.0 s inference and 18 graphs, 20.3 s training. Build GT sequence inputs from tensor dicts instead of an internal HeteroData #809 14 graphs, 2.6s inference and 14 graphs, 4.5 s training. This PR 23 graphs, 25.6 s inference and 21 graphs, 15.5 s training.
Headlines:
origin/maineager to this PR eager):torch.compile(origin/maineager to this PR's best compiled variant):torch.compile(encoder)(4.3x), peak 1.33 GiB.🤖 Generated with Claude Code