| # Incremental migration plan: `root_gnn_dgl` |
|
|
| The target is `legacy/root_gnn_dgl/`. `legacy/physicsnemo/` is a prior rewrite |
| attempt and may inspire abstractions, but it is not a parity target. Neither |
| legacy tree should be modified during migration. |
|
|
| Each phase should add focused unit tests, a deterministic fixture in |
| `data/fixtures/`, and parity tests under `tests/parity/` before moving upward. |
| Record intentional differences and checkpoint consequences here. |
|
|
| ## Phase 0 — freeze observations and fixtures |
|
|
| Capture a small representative ROOT-equivalent fixture containing the seven |
| active node features, three edge features, labels, fold values, weights, and |
| globals. Record outputs of `node_features_from_tree`, `full_connected_graph`, |
| `EdgeDataset.make_graph`, and `fold_selection` |
| ([`dataset.py:15-59`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py), |
| [`dataset.py:471-482`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py), |
| [`utils.py:121-143`](../legacy/root_gnn_dgl/root_gnn_base/utils.py)). Preserve |
| one `.bin`, one `model_epoch_N.pt`, one evaluation `.npz`, and one inference |
| `.npz` fixture if available. |
|
|
| Task 3 characterization records the active-path observations. Node rows are |
| concatenated by object type in the configured order (jets, electrons, muons, |
| photons, MET), and the seven columns are `[pt, eta, phi, energy, btag, charge, |
| node_type]`. `CALC_E` is `pt*cosh(eta)` before the configured column scale is |
| applied. The graph is directed and uses all ordered pairs except self-loops |
| for graphs with more than one node; edge order is source-major. A one-node |
| graph is a special case: the no-self-loop branch retains its sole self-loop. |
| Edge columns are `[deta, dphi, dR]`, with `dphi` wrapped into `[-pi, pi]`. |
| Dataset items expose `(graph, label, tracking, global_features)`; tracking |
| column 0 is the fold identifier and column 1 is the event weight. These are |
| compatibility observations, not proposed fixes. |
| |
| ## Phase 1 — configuration boundary |
| |
| Implement a typed configuration layer that reads `Training`, `Model`, |
| optional `Loss`, and `Datasets`. Initially retain a compatibility adapter for |
| `module`/`class`/`args` and runtime injection of `sample_graph` and |
| `sample_global`, matching `buildFromConfig` |
| ([`utils.py:10-43`](../legacy/root_gnn_dgl/root_gnn_base/utils.py)). Keep |
| dynamic imports isolated at this boundary rather than spreading reflection |
| through new code. |
| |
| ## Phase 2 — pure preprocessing parity |
| |
| Port and test, in isolation: |
| |
| - branch-to-node conversion, `CALC_E`, `NODE_TYPE`, constants, scaling, empty |
| objects, and dtypes (`node_features_from_tree`, |
| [`dataset.py:15-50`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py)); |
| - string/tuple selections and cutflow (`check_selection`, `selection_mask`, |
| `compute_cutflow`, [`dataset.py:75-158`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py)); |
| - fold masks and cache suffixes (`fold_selection`, `fold_selection_name`, |
| [`utils.py:121-143`](../legacy/root_gnn_dgl/root_gnn_base/utils.py)); |
| - deterministic chunk partitioning (`hash_partition`, |
| [`batched_dataset.py:27-31`](../legacy/root_gnn_dgl/root_gnn_base/batched_dataset.py)). |
| |
| This is the highest-value parity layer: model parity is invalid if graph inputs |
| differ. |
| |
| Task 4 implements the shared branch-to-node feature builder under |
| `gnn4colliders.features`. It preserves the active seven-column schema, |
| object-type ordering, explicit scales, derived `CALC_E`, node-type codes, |
| float32 output, and supported empty vector collections. Selection, fold, and |
| chunk helpers remain deferred to later data-infrastructure work. |
| |
| Task 6 implements the shared ROOT/Awkward ingestion boundary under |
| `gnn4colliders.data`. `RootEventDataset` returns immutable, architecture-neutral |
| `EventSample` values with selected branch data, labels, tracking, and globals; |
| events are ordered by input file order with a global zero-based index. Fold |
| filtering, caching, batching, and model-specific conversion remain deferred. |
| |
| ## Phase 3 — graph construction and cache format |
| |
| Implement graph construction with tests for node/edge counts, directed edge |
| ordering, self-loop policy, `[deta, dphi, dR]` order, metadata, and empty graphs. |
| Preserve the dataset item contract `(graph, label, tracking, global_features)` |
| from `RootDataset.__getitem__` |
| ([`dataset.py:465-469`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py)). |
| |
| Then implement DGL `.bin` serialization, lazy chunk loading, pre-batching, and |
| padding. Compare against `RootDataset.save/load`, `LazyDataset`, and |
| `PreBatchedDataset` ([`dataset.py:396-469`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py), |
| [`batched_dataset.py:129-174`](../legacy/root_gnn_dgl/root_gnn_base/batched_dataset.py)). |
| Treat `NONE`, `STEPS`, `FIXED`, and `NODE` as explicit features; do not hide |
| the hardcoded fixed padding sizes. |
| |
| Task 7 establishes the metadata-aware orchestration boundary around this |
| phase: `EventMetadata`, `GraphSample`, `GraphBatch`, fold-based split |
| selection, deterministic batching, and a version-checked graph-sample cache. |
| The cache is deliberately Level 2; normalized event caching remains a future |
| extension so non-graph model families can reuse ROOT preprocessing. |
| |
| ## Phase 4 — active model parity |
| |
| Task 8 adds the active `EdgeNetwork` and `FineTunedEdgeNetwork` under |
| `gnn4colliders.models.root_gnn`. The update order and MLP ordering follow the |
| legacy active path. The rewrite uses an explicit backbone/classifier boundary, |
| local DGL graph scope, and does not mutate global RNG state in constructors. |
| Model parity now covers fixed-weight pretraining and transfer paths, including |
| historical checkpoint prefixes. The legacy transfer implementation has an |
| active bug when nonempty globals are supplied (`Pretrained_Output` ignores its |
| argument); parity therefore characterizes its supported no-global path, while |
| the rewritten model supports both global and fallback modes. |
| |
| Port `models.GCN.Edge_Network` first. Preserve constructor parameters, |
| `forward(graph, global_feats)`, feature keys, processor order, MLP LayerNorm |
| placement, and logits shape. Compare intermediate and final tensors on fixed |
| graphs using the legacy architecture |
| ([`GCN.py:18-35`](../legacy/root_gnn_dgl/models/GCN.py), |
| [`GCN.py:182-251`](../legacy/root_gnn_dgl/models/GCN.py)). |
| |
| Next port `Transferred_Learning_Finetuning`, including pretrained |
| `model_state_dict` loading, removal of the final classifier, and new classifier |
| initialization ([`GCN.py:884-997`](../legacy/root_gnn_dgl/models/GCN.py)). Test |
| both frozen and unfrozen modes. Defer other model classes until an active |
| config or consumer proves they are needed. |
| |
| ## Phase 5 — objectives and metrics |
| |
| Implement the default objective exactly: elementwise configured loss, |
| tracking-column weights, per-unique-label normalization, and averaging across |
| labels ([`training_script.py:320-359`](../legacy/root_gnn_dgl/scripts/training_script.py)). |
| Add parity cases for positive, zero, and negative weights and binary versus |
| multiclass shapes. |
| |
| Port metric behavior from |
| [`training_script.py:438-510`](../legacy/root_gnn_dgl/scripts/training_script.py): |
| sigmoid threshold 0.5, argmax, weight masking, weighted ROC AUC, one-vs-rest |
| multiclass AUC, and NaN behavior when AUC is undefined. Add `models/loss.py` |
| classes only with dedicated tests; do not substitute their reductions. |
| |
| ## Phase 6 — checkpoint and lifecycle |
| |
| Task 10 implemented the in-memory single-process training lifecycle before the |
| checkpoint portion of this phase: `Trainer`, explicit optimizer/scheduler |
| builders, `EarlyStopping`, reproducibility seeding, `GraphBatch.to`, and |
| epoch/history result types. Checkpoint persistence/resume and the Python |
| inference/evaluation and named NPZ/ROOT output layers are now implemented. |
| Distributed execution and CLI wiring were completed in the later phases. |
| |
| Task 10 also establishes corrected split semantics: validation is evaluated |
| every epoch and is the only split used for model selection or early stopping; |
| the test split remains held out and is evaluated separately after fitting. The |
| legacy loader naming inversion (`test` used for selection and `val` held out) |
| is not carried into the rewrite. |
| |
| Create a checkpoint adapter preserving `model_epoch_<epoch>.pt` and keys |
| `epoch`, `model_state_dict`, `optimizer_state_dict`, and `early_stop` |
| ([`training_script.py:565-604`](../legacy/root_gnn_dgl/scripts/training_script.py)). |
| Support legacy DDP/compiled prefixes (`module.` and `_orig_mod.`) as exercised |
| by checkpoint lookup and inference |
| ([`utils.py:145-248`](../legacy/root_gnn_dgl/root_gnn_base/utils.py), |
| [`inference.py:274-290`](../legacy/root_gnn_dgl/scripts/inference.py)). Port |
| `EarlyStop` state and log parsing separately |
| ([`utils.py:325-390`](../legacy/root_gnn_dgl/root_gnn_base/utils.py)). Verify |
| resume, restart, early termination, and `.npz` fields before distributed work. |
| |
| ## Phase 7 — CLI, inference, and export |
| |
| Task 12 implemented ordered prediction/evaluation, task-owned score |
| semantics, checkpoint weight-only loading, named metadata retention, NPZ |
| output, and explicit ROOT entry alignment. The semantic CLI and the validated |
| ROOT-GNN ONNX export adapter are implemented. |
| |
| Task 13 adds Hydra composition and a single-process CLI around those existing |
| APIs. The current application data boundary is a versioned |
| `GraphSampleCache`; ROOT preparation converts events through the shared |
| feature and graph builders before writing that cache. |
| |
| Build thin new applications around tested library interfaces in this order: |
| |
| 1. preprocessing/cache generation (`scripts/prep_data.py`); |
| 2. training/evaluation (`scripts/training_script.py`); |
| 3. inference to `.npz` and ROOT (`scripts/inference.py`); |
| 4. ONNX export after PyTorch parity (`gnn4colliders export`). |
| |
| Use subprocess integration tests with tiny fixtures. Preserve CLI options only |
| where they serve an active workflow; document removed diagnostic/cluster-only |
| options. |
| |
| ## Phase 8 — reproducibility and deployment |
| |
| Task 14 adds the initial deployment boundary: CPU/GPU DDP through standard |
| `torchrun` variables, rank-local graph-sample sharding, global metric/output |
| gathering, rank-0 checkpoint/config writing, and Perlmutter-oriented Slurm |
| examples. Evaluation deliberately avoids sampler padding duplicates. The |
| remaining follow-up is a streaming or sharded output path for very large |
| distributed inference jobs. |
| |
| The seed policy remains explicit: the configured seed is offset by rank for |
| process-local randomness, while distributed sample assignment is derived from |
| the configured seed, world size, and epoch. GPU kernel nondeterminism and |
| exact per-rank RNG checkpoint replay remain environment-dependent. Slurm/NCCL, |
| Podman-HPC, ROOT, and Hugging Face integrations stay in launcher/adapters |
| rather than package code. |
| |
| ## Checkpoint compatibility checklist |
| |
| - [x] Load a checked-in or generated multiclass pretrained checkpoint. |
| - [x] Load a legacy fine-tuning checkpoint after prefix normalization. |
| - [x] Resume optimizer and early-stop state. |
| - [x] Produce equivalent logits on a deterministic graph fixture. |
| - [x] Produce equivalent `.npz` score, label, and metadata fields. |
| - [x] Preserve ROOT scalar/vector score branch conventions in the Python adapter. |
| |
| Known risks are documented in [`architecture.md`](architecture.md): edge order, |
| self-loops, weight semantics, validation/test naming, padding, dynamic |
| selection evaluation, reproducibility, and the experimental model/loss surface. |
| |
| ## Migration closure status |
| |
| ### Task 18 compatibility closure |
| |
| The compatibility boundary is now explicit in `gnn4colliders.compat`. |
| Production ingestion stores named `EventMetadata`; legacy two-column tracking |
| is converted only at the compatibility boundary. Checkpoint prefix cleanup and |
| the historical ROOT-GNN `classify` to `classifier` mapping have one canonical |
| implementation. The new checkpoint schema and named NPZ output remain |
| canonical. See [`compatibility.md`](compatibility.md) for the supported and |
| intentionally unsupported historical artifacts. |
| |
| The following matrix describes the supported new stack, rather than every |
| class that exists in `legacy/`: |
| |
| | Legacy area | New-stack status | Notes | |
| | --- | --- | --- | |
| | ROOT/Awkward ingestion | migrated | `RootEventDataset` returns `EventSample` in file/event order | |
| | node features | migrated + parity-tested | seven-column schema, `CALC_E`, ordering, scales, float32 | |
| | edge construction | migrated + parity-tested | directed source-major topology and `[deta,dphi,dR]` | |
| | graph cache | migrated | versioned `GraphSampleCache`; graph-level cache only | |
| | folds and weights | migrated | named `EventMetadata.fold` and `.weight` | |
| | batching | migrated | deterministic local loader and DDP sharding | |
| | legacy padding modes | deferred | no active new-stack consumer | |
| | `Edge_Network` | migrated + parity-tested | `EdgeNetwork`, raw logits | |
| | transfer/fine-tuning | migrated + parity-tested | frozen or trainable backbone | |
| | loss and metrics | migrated + parity-tested | task-owned weighted reductions and full-split AUC | |
| | training lifecycle | migrated | `Trainer`, validation semantics, scheduler, early stopping | |
| | checkpoints/resume | migrated | schema v1; historical weight/prefix adapter | |
| | inference/NPZ | migrated | named output fields and ordered accumulation | |
| | ROOT score output | compatibility adapter | Python API supported; CLI currently NPZ-only | |
| | DDP | migrated | torchrun boundary, rank-0 artifacts, gathered metrics | |
| | Slurm/Perlmutter | launcher examples | site policy remains outside package code | |
| | ONNX export | migrated for ROOT-GNN | tensor-only adapter, ONNX Runtime validation, and `export` CLI; raw graph tensors are the input contract | |
| |
| ### Intentional redesigns |
| |
| These are deliberate new-stack contracts, not accidental parity failures: |
| |
| * `tracking[:, 0]` and `tracking[:, 1]` become named `metadata.fold` and |
| `metadata.weight`; public consumers do not depend on positional columns. |
| * Dynamic legacy YAML `module`/`class` construction becomes allow-listed |
| semantic Hydra configuration. |
| * The monolithic training script becomes `Task` + `Trainer` + checkpoint and |
| inference adapters. |
| * Graph state is scoped to the forward pass rather than relying on persistent |
| mutation of shared graph state. |
| * Model constructors do not mutate global RNG state; seeding is explicit in |
| the training/application boundary. |
| * Validation is the selection/early-stopping split and test is held out. This |
| corrects the legacy loader-name inversion. |
|
|
| Compatibility preserves externally observable scientific behavior where it is |
| validated; it does not promise to preserve every legacy implementation bug. |
| The characterized legacy transfer path had a nonempty-global handling bug; |
| the rewrite supports named globals. Negative weights, rare empty graphs, |
| historical checkpoint variants, and legacy padding edge cases remain areas to |
| audit when a supported consumer requires them. |
|
|
| ## ROOT-GNN v1 completion checklist |
|
|
| - [x] active ROOT data path and graph cache |
| - [x] validated feature, graph, model, task, and metric behavior |
| - [x] train from scratch and fine-tune a pretrained backbone |
| - [x] resume new-stack checkpoints and load supported historical weights |
| - [x] evaluate and predict named outputs |
| - [x] single-process and DDP application boundaries |
| - [x] Perlmutter/Slurm launcher examples and profiling guidance |
| - [x] ROOT-GNN ONNX export and CPU Runtime parity |
| - [ ] streaming/sharded large-scale prediction output |
| - [ ] removal of frozen legacy reference |
| - [ ] ROOT-Transformer representation/model |
|
|
| ROOT-GNN v1 is complete when the checked-in new stack can prepare active data, |
| reproduce validated legacy behavior, train, transfer, resume, evaluate, |
| predict, and run single-process or DDP workflows. The remaining unchecked |
| items are intentionally deferred rather than undocumented promises. |
|
|