File size: 16,265 Bytes
916755e 5ae003d 916755e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | # 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.
Task 22 adds `TensorEdgeNetwork`, a native PyTorch realization of the same
message-passing equations. It receives explicit node/edge index tensors from
the batching boundary, retains parameter names for legacy checkpoint loading,
and has fixed-weight CPU parity coverage against the DGL backend. It is the
default training backend for CPU, CUDA, and Apple MPS; DGL remains available as
a cache and compatibility adapter during the transition.
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.
|