feat: upgrade to Python 3.14 and PyTorch 2.14; add torch.compile and bfloat16
Browse files- Python 3.14, torch 2.14.1 (cu130 wheels), NumPy 2; frozen parity
references pass unchanged
- trainer.compile (dynamic-shape torch.compile) and trainer.precision
(bfloat16 autocast; graph sums, loss, optimizer in float32)
- CUDA graph steps use device-tensor learning rates, so one capture per
bucket follows the schedule; checkpoints store portable optimizer state
- trainer=paper_fast: compile + CUDA graphs + bfloat16, 5.9 vs 19.2 ms per
GamGam batch for float32 eager on an A100
- loader workers fork explicitly (Python 3.14 defaults to forkserver);
ONNX export keeps the TorchScript exporter
Correction to 5b73bcb: the pre-change float32 eager step was 21.3 ms per
batch (not ~29 ms; that profile used unshuffled ttH-only batches), so the
default path improved by about 1.2x and the large gains are opt-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- .python-version +1 -1
- CHANGELOG.md +10 -2
- README_PROJECT.md +2 -1
- docs/configuration.md +15 -8
- docs/migration.md +8 -0
- docs/performance.md +38 -3
- docs/quickstart.md +4 -3
- pyproject.toml +6 -6
- src/gnn4colliders/config/application.py +1 -1
- src/gnn4colliders/config/factories.py +2 -0
- src/gnn4colliders/configs/trainer/default.yaml +4 -0
- src/gnn4colliders/configs/trainer/paper.yaml +2 -0
- src/gnn4colliders/configs/trainer/paper_fast.yaml +7 -3
- src/gnn4colliders/data/loader.py +4 -0
- src/gnn4colliders/export/onnx.py +3 -0
- src/gnn4colliders/models/root_gnn/edge_network.py +7 -4
- src/gnn4colliders/training/checkpoint.py +16 -1
- src/gnn4colliders/training/cuda_graphs.py +28 -18
- src/gnn4colliders/training/trainer.py +38 -3
- tests/fixtures/root_gnn_dgl_reference/generate.py +1 -1
- tests/unit/training/test_lifecycle_policies.py +59 -0
- uv.lock +0 -0
|
@@ -1 +1 @@
|
|
| 1 |
-
3.
|
|
|
|
| 1 |
+
3.14
|
|
@@ -11,8 +11,10 @@
|
|
| 11 |
selection (`checkpoint=best_auc`), and an ensemble Slurm script.
|
| 12 |
- `data.derived_branches` (object selection and tag thresholds) and
|
| 13 |
`data.fold_modulus` for ntuples outside the Delphes layout.
|
| 14 |
-
- Opt-in
|
| 15 |
-
(`trainer.
|
|
|
|
|
|
|
| 16 |
- `data.loader.num_workers`/`pin_memory` for background batch collation.
|
| 17 |
- Per-epoch `history.jsonl`, `inference.metrics_output`, early stopping on
|
| 18 |
`train_loss`, and macro precision/recall/F1 for multiclass tasks.
|
|
@@ -21,6 +23,12 @@
|
|
| 21 |
|
| 22 |
### Changed
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
- DGL was removed. Preparation is vectorized and writes a model-neutral packed
|
| 25 |
`EventCache` (cache schema 2) instead of pickled DGL graphs; graph batches
|
| 26 |
(`TensorGraph`, edge-index layout) and transformer token batches are built
|
|
|
|
| 11 |
selection (`checkpoint=best_auc`), and an ensemble Slurm script.
|
| 12 |
- `data.derived_branches` (object selection and tag thresholds) and
|
| 13 |
`data.fold_modulus` for ntuples outside the Delphes layout.
|
| 14 |
+
- Opt-in `torch.compile` (`trainer.compile`), bfloat16 autocast
|
| 15 |
+
(`trainer.precision`), CUDA graph training steps (`trainer.cuda_graphs`), and
|
| 16 |
+
TF32 matmuls (`trainer.allow_tf32`); `trainer=paper_fast` enables all four,
|
| 17 |
+
about 3x faster per GamGam epoch than float32 eager on an A100.
|
| 18 |
- `data.loader.num_workers`/`pin_memory` for background batch collation.
|
| 19 |
- Per-epoch `history.jsonl`, `inference.metrics_output`, early stopping on
|
| 20 |
`train_loss`, and macro precision/recall/F1 for multiclass tasks.
|
|
|
|
| 23 |
|
| 24 |
### Changed
|
| 25 |
|
| 26 |
+
- The environment moved to Python 3.14 and PyTorch 2.14.1 (CUDA 13.0 wheels;
|
| 27 |
+
NumPy 2), which is required for `torch.compile`. Frozen ROOT-GNN parity
|
| 28 |
+
references are reproduced unchanged. ONNX export keeps the TorchScript
|
| 29 |
+
exporter (`dynamo=False`), and loader workers are explicitly forked because
|
| 30 |
+
Python 3.14 no longer forks by default.
|
| 31 |
+
|
| 32 |
- DGL was removed. Preparation is vectorized and writes a model-neutral packed
|
| 33 |
`EventCache` (cache schema 2) instead of pickled DGL graphs; graph batches
|
| 34 |
(`TensorGraph`, edge-index layout) and transformer token batches are built
|
|
@@ -21,7 +21,8 @@ backend.
|
|
| 21 |
|
| 22 |
## Installation
|
| 23 |
|
| 24 |
-
The supported development environment is Python 3.
|
|
|
|
| 25 |
development is supported on macOS and Linux:
|
| 26 |
|
| 27 |
```bash
|
|
|
|
| 21 |
|
| 22 |
## Installation
|
| 23 |
|
| 24 |
+
The supported development environment is Python 3.14 (`>=3.14,<3.15`) with
|
| 25 |
+
PyTorch 2.14 (CUDA 13.0 wheels on Linux x86-64). Core
|
| 26 |
development is supported on macOS and Linux:
|
| 27 |
|
| 28 |
```bash
|
|
@@ -150,14 +150,21 @@ distributed preparation and should be benchmarked on the target machine.
|
|
| 150 |
|
| 151 |
## Training speed options
|
| 152 |
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
## Checkpoints and resolved configuration
|
| 163 |
|
|
|
|
| 150 |
|
| 151 |
## Training speed options
|
| 152 |
|
| 153 |
+
| Option | Effect |
|
| 154 |
+
| --- | --- |
|
| 155 |
+
| `trainer.compile: true` | `torch.compile` the model forward with dynamic shapes (Inductor); the first epoch includes about a minute of compilation |
|
| 156 |
+
| `trainer.precision: bfloat16` | bfloat16 autocast for the forward pass; parameters, optimizer, loss, and graph sums stay float32 |
|
| 157 |
+
| `trainer.allow_tf32: true` | TensorFloat-32 for float32 CUDA matrix multiplication |
|
| 158 |
+
| `trainer.cuda_graphs: true` | capture complete training steps (forward, backward, Adam step) as CUDA graphs and replay them |
|
| 159 |
+
|
| 160 |
+
For CUDA graphs, graph batches are padded into node/edge capacity buckets with
|
| 161 |
+
discarded padding graphs, and learning rates become device tensors that
|
| 162 |
+
schedulers update in place, so one capture per bucket serves the whole run.
|
| 163 |
+
They require a CUDA device, a single process, a graph model, and Adam or
|
| 164 |
+
AdamW; checkpoints store plain learning rates and resume in any mode. All four
|
| 165 |
+
options are off by default, and with any of them results agree with float32
|
| 166 |
+
eager training to floating-point rounding rather than bitwise.
|
| 167 |
+
`trainer=paper_fast` enables all four. See [performance.md](performance.md).
|
| 168 |
|
| 169 |
## Checkpoints and resolved configuration
|
| 170 |
|
|
@@ -114,6 +114,14 @@ Intentional differences from the previous main:
|
|
| 114 |
- the scheduler is now stepped before the epoch hook rather than after the
|
| 115 |
early-stopping check; the stopping epoch still does not step it.
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
## Validation requirements
|
| 118 |
|
| 119 |
Every new model family must provide unit tests for its representation and
|
|
|
|
| 114 |
- the scheduler is now stepped before the epoch hook rather than after the
|
| 115 |
early-stopping check; the stopping epoch still does not step it.
|
| 116 |
|
| 117 |
+
Environment upgrade (Python 3.12 / PyTorch 2.2.2 to Python 3.14 / PyTorch
|
| 118 |
+
2.14.1, CUDA 13.0, NumPy 2): every frozen parity reference, including the
|
| 119 |
+
bitwise DGL forward outputs and prepared graphs, passes unchanged. ONNX export
|
| 120 |
+
passes `dynamo=False` to keep the TorchScript exporter and its `dynamic_axes`
|
| 121 |
+
contract (the default dynamo exporter needs `onnxscript`); it emits deprecation
|
| 122 |
+
warnings. CUDA graph steps hold learning rates as device tensors; checkpoints
|
| 123 |
+
store them as floats with `capturable` disabled.
|
| 124 |
+
|
| 125 |
## Validation requirements
|
| 126 |
|
| 127 |
Every new model family must provide unit tests for its representation and
|
|
@@ -28,9 +28,44 @@ threads) against the previous DGL graph cache:
|
|
| 28 |
| Batch construction (1024 events) | 83 ms | 3.3 ms |
|
| 29 |
| GPU training epoch | 166.5 ms/batch | 25.8 ms/batch |
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
affect ordering or output semantics.
|
| 35 |
|
| 36 |
## CPU threads
|
|
|
|
| 28 |
| Batch construction (1024 events) | 83 ms | 3.3 ms |
|
| 29 |
| GPU training epoch | 166.5 ms/batch | 25.8 ms/batch |
|
| 30 |
|
| 31 |
+
At that point eager training became bound by host-side kernel launches (about
|
| 32 |
+
a thousand small kernels per step for this model), not by GPU arithmetic.
|
| 33 |
+
|
| 34 |
+
## GamGam training step
|
| 35 |
+
|
| 36 |
+
Measured on the ATLAS Open Data GamGam task (1024-event batches of about 5,600
|
| 37 |
+
nodes and 30,000 edges; paper model: hidden size 64, 4 MLP layers, 4 graph
|
| 38 |
+
steps), one A100-SXM4-80GB, `OMP_NUM_THREADS=8`, loader workers with pinned
|
| 39 |
+
memory, steady-state `Trainer.train_epoch` over 300 shuffled batches:
|
| 40 |
+
|
| 41 |
+
| Options | torch 2.2.2, Python 3.12 | torch 2.14.1, Python 3.14 |
|
| 42 |
+
| --- | ---: | ---: |
|
| 43 |
+
| float32 eager (default) | 18.4 ms | 19.2 ms |
|
| 44 |
+
| bfloat16 eager | | 22.4 ms |
|
| 45 |
+
| CUDA graphs + TF32 | 8.1 ms | 8.5 ms |
|
| 46 |
+
| `torch.compile` | n/a | 13.0 ms |
|
| 47 |
+
| compile + CUDA graphs + TF32 | n/a | 6.1 ms |
|
| 48 |
+
| compile + CUDA graphs + bfloat16 (`paper_fast`) | n/a | 5.9 ms |
|
| 49 |
+
|
| 50 |
+
Before this work the float32 eager step (collation, transfer, and step on the
|
| 51 |
+
training thread) took 21.3 ms per batch against 17.7 ms now with the same
|
| 52 |
+
torch 2.2.2 stack, and epoch metrics used a pairwise ROC AUC that could not
|
| 53 |
+
evaluate the 2M-event training split at all. Training-set AUC after three epochs agreed
|
| 54 |
+
across all rows to within GPU run-to-run variation (0.862-0.867). A full
|
| 55 |
+
`trainer=paper_fast` epoch, including validation, takes about 12.5 s (37 s for
|
| 56 |
+
the first, which compiles) against about 38 s for float32 eager.
|
| 57 |
+
|
| 58 |
+
Observations:
|
| 59 |
+
|
| 60 |
+
- bfloat16 alone is slower: the model is small (64 features), so it is not
|
| 61 |
+
matrix-multiplication bound, and eager autocast adds cast kernels. Under
|
| 62 |
+
`torch.compile` the casts fuse and bfloat16 is on par with TF32.
|
| 63 |
+
- `torch.compile` alone removes about a third of the launch overhead; CUDA
|
| 64 |
+
graphs remove the rest, and the two compose.
|
| 65 |
+
- Graph sums accumulate in float32 under autocast (bfloat16 represents
|
| 66 |
+
integers exactly only to 256, and means divide by edge counts).
|
| 67 |
+
|
| 68 |
+
Size-aware batching and streaming prediction remain unexplored because they
|
| 69 |
affect ordering or output semantics.
|
| 70 |
|
| 71 |
## CPU threads
|
|
@@ -12,7 +12,8 @@ ROOT -> features -> EventCache (packed objects) -> graph or sequence batch -> mo
|
|
| 12 |
|
| 13 |
## Install
|
| 14 |
|
| 15 |
-
The supported environment is Python 3.
|
|
|
|
| 16 |
|
| 17 |
```bash
|
| 18 |
uv sync --dev
|
|
@@ -77,8 +78,8 @@ uv run gnn4colliders evaluate --config-name config_atlas_opendata_gamgam \
|
|
| 77 |
|
| 78 |
Preparation takes about 15 s with 8 workers. Training follows the paper
|
| 79 |
recipe and keeps `best.pt`, the epoch with the best validation AUC; add
|
| 80 |
-
`trainer=paper_fast` for CUDA graph replay and
|
| 81 |
-
epoch on an A100). The task, its configuration choices, and the comparison
|
| 82 |
with the paper are described in
|
| 83 |
[atlas_opendata_gamgam.md](atlas_opendata_gamgam.md).
|
| 84 |
|
|
|
|
| 12 |
|
| 13 |
## Install
|
| 14 |
|
| 15 |
+
The supported environment is Python 3.14 with PyTorch 2.14. From the
|
| 16 |
+
repository root:
|
| 17 |
|
| 18 |
```bash
|
| 19 |
uv sync --dev
|
|
|
|
| 78 |
|
| 79 |
Preparation takes about 15 s with 8 workers. Training follows the paper
|
| 80 |
recipe and keeps `best.pt`, the epoch with the best validation AUC; add
|
| 81 |
+
`trainer=paper_fast` for `torch.compile`, CUDA graph replay, and a bfloat16
|
| 82 |
+
forward (about 3x faster per epoch on an A100). The task, its configuration choices, and the comparison
|
| 83 |
with the paper are described in
|
| 84 |
[atlas_opendata_gamgam.md](atlas_opendata_gamgam.md).
|
| 85 |
|
|
@@ -7,14 +7,14 @@ name = "gnn4colliders"
|
|
| 7 |
dynamic = ["version"]
|
| 8 |
description = "Machine-learning tools for collider event analysis."
|
| 9 |
readme = "README.md"
|
| 10 |
-
requires-python = ">=3.
|
| 11 |
dependencies = [
|
| 12 |
"awkward>=2.0",
|
| 13 |
"hydra-core>=1.3",
|
| 14 |
"huggingface-hub>=0.36,<1",
|
| 15 |
-
"numpy>=
|
| 16 |
"scikit-learn>=1.3",
|
| 17 |
-
"torch==2.
|
| 18 |
"uproot>=5.0",
|
| 19 |
]
|
| 20 |
|
|
@@ -49,12 +49,12 @@ version = { attr = "gnn4colliders.__version__" }
|
|
| 49 |
|
| 50 |
[tool.uv.sources]
|
| 51 |
torch = [
|
| 52 |
-
{ index = "pytorch-
|
| 53 |
]
|
| 54 |
|
| 55 |
[[tool.uv.index]]
|
| 56 |
-
name = "pytorch-
|
| 57 |
-
url = "https://download.pytorch.org/whl/
|
| 58 |
explicit = true
|
| 59 |
|
| 60 |
[tool.pytest.ini_options]
|
|
|
|
| 7 |
dynamic = ["version"]
|
| 8 |
description = "Machine-learning tools for collider event analysis."
|
| 9 |
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.14,<3.15"
|
| 11 |
dependencies = [
|
| 12 |
"awkward>=2.0",
|
| 13 |
"hydra-core>=1.3",
|
| 14 |
"huggingface-hub>=0.36,<1",
|
| 15 |
+
"numpy>=2.1",
|
| 16 |
"scikit-learn>=1.3",
|
| 17 |
+
"torch==2.14.1",
|
| 18 |
"uproot>=5.0",
|
| 19 |
]
|
| 20 |
|
|
|
|
| 49 |
|
| 50 |
[tool.uv.sources]
|
| 51 |
torch = [
|
| 52 |
+
{ index = "pytorch-cu130", marker = "sys_platform == 'linux' and platform_machine == 'x86_64'" },
|
| 53 |
]
|
| 54 |
|
| 55 |
[[tool.uv.index]]
|
| 56 |
+
name = "pytorch-cu130"
|
| 57 |
+
url = "https://download.pytorch.org/whl/cu130"
|
| 58 |
explicit = true
|
| 59 |
|
| 60 |
[tool.pytest.ini_options]
|
|
@@ -287,7 +287,7 @@ class _CheckpointPolicy:
|
|
| 287 |
"epoch": epoch,
|
| 288 |
"seconds": now - self.started,
|
| 289 |
"learning_rate": [
|
| 290 |
-
group["lr"] for group in self.trainer.optimizer.param_groups
|
| 291 |
],
|
| 292 |
"train": {"loss": train_result.loss, **train_result.metrics},
|
| 293 |
}
|
|
|
|
| 287 |
"epoch": epoch,
|
| 288 |
"seconds": now - self.started,
|
| 289 |
"learning_rate": [
|
| 290 |
+
float(group["lr"]) for group in self.trainer.optimizer.param_groups
|
| 291 |
],
|
| 292 |
"train": {"loss": train_result.loss, **train_result.metrics},
|
| 293 |
}
|
|
@@ -179,4 +179,6 @@ def build_trainer(
|
|
| 179 |
scheduler_step=scheduler_step,
|
| 180 |
distributed_context=distributed_context,
|
| 181 |
cuda_graphs=bool(_get(config, "cuda_graphs", False)),
|
|
|
|
|
|
|
| 182 |
)
|
|
|
|
| 179 |
scheduler_step=scheduler_step,
|
| 180 |
distributed_context=distributed_context,
|
| 181 |
cuda_graphs=bool(_get(config, "cuda_graphs", False)),
|
| 182 |
+
compile_model=bool(_get(config, "compile", False)),
|
| 183 |
+
precision=str(_get(config, "precision", "float32")),
|
| 184 |
)
|
|
@@ -18,3 +18,7 @@ early_stopping:
|
|
| 18 |
# a single process, graph batches, and an Adam optimizer.
|
| 19 |
allow_tf32: false
|
| 20 |
cuda_graphs: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
# a single process, graph batches, and an Adam optimizer.
|
| 19 |
allow_tf32: false
|
| 20 |
cuda_graphs: false
|
| 21 |
+
# torch.compile the model forward (dynamic shapes), and the forward precision:
|
| 22 |
+
# float32 or bfloat16 (autocast; loss, graph sums, and optimizer in float32).
|
| 23 |
+
compile: false
|
| 24 |
+
precision: float32
|
|
@@ -19,3 +19,5 @@ early_stopping:
|
|
| 19 |
min_delta: 0.0
|
| 20 |
allow_tf32: false
|
| 21 |
cuda_graphs: false
|
|
|
|
|
|
|
|
|
| 19 |
min_delta: 0.0
|
| 20 |
allow_tf32: false
|
| 21 |
cuda_graphs: false
|
| 22 |
+
compile: false
|
| 23 |
+
precision: float32
|
|
@@ -2,7 +2,11 @@ defaults:
|
|
| 2 |
- paper
|
| 3 |
- _self_
|
| 4 |
|
| 5 |
-
# The paper recipe with captured CUDA graph steps and
|
| 6 |
-
# faster per epoch
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
cuda_graphs: true
|
|
|
|
|
|
|
|
|
| 2 |
- paper
|
| 3 |
- _self_
|
| 4 |
|
| 5 |
+
# The paper recipe with a compiled model, captured CUDA graph steps, and a
|
| 6 |
+
# bfloat16 forward: about 3x faster per epoch than float32 eager on an A100
|
| 7 |
+
# for the GamGam task (docs/performance.md). Results agree with float32 to
|
| 8 |
+
# rounding, not bitwise.
|
| 9 |
+
compile: true
|
| 10 |
cuda_graphs: true
|
| 11 |
+
precision: bfloat16
|
| 12 |
+
allow_tf32: true
|
|
@@ -114,6 +114,10 @@ class EventLoader:
|
|
| 114 |
num_workers=self.num_workers,
|
| 115 |
pin_memory=self.pin_memory,
|
| 116 |
prefetch_factor=self.prefetch_factor,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
)
|
| 118 |
|
| 119 |
|
|
|
|
| 114 |
num_workers=self.num_workers,
|
| 115 |
pin_memory=self.pin_memory,
|
| 116 |
prefetch_factor=self.prefetch_factor,
|
| 117 |
+
# Forked workers share the events copy-on-write; Python 3.14's
|
| 118 |
+
# default (forkserver) would pickle them to every worker. Workers
|
| 119 |
+
# only collate on the CPU, so forking a CUDA process is safe.
|
| 120 |
+
multiprocessing_context="fork",
|
| 121 |
)
|
| 122 |
|
| 123 |
|
|
@@ -304,6 +304,9 @@ def export_root_gnn_onnx(
|
|
| 304 |
"logits": {0: "graphs"},
|
| 305 |
},
|
| 306 |
do_constant_folding=True,
|
|
|
|
|
|
|
|
|
|
| 307 |
)
|
| 308 |
validate_onnx(temporary)
|
| 309 |
with torch.inference_mode():
|
|
|
|
| 304 |
"logits": {0: "graphs"},
|
| 305 |
},
|
| 306 |
do_constant_folding=True,
|
| 307 |
+
# The TorchScript exporter keeps the dynamic_axes contract and
|
| 308 |
+
# needs no onnxscript dependency.
|
| 309 |
+
dynamo=False,
|
| 310 |
)
|
| 311 |
validate_onnx(temporary)
|
| 312 |
with torch.inference_mode():
|
|
@@ -15,8 +15,10 @@ from .blocks import make_mlp
|
|
| 15 |
def _sum_by_group(
|
| 16 |
values: torch.Tensor, groups: torch.Tensor, count: int
|
| 17 |
) -> torch.Tensor:
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
def _gather(values: torch.Tensor, index: torch.Tensor) -> torch.Tensor:
|
|
@@ -30,8 +32,9 @@ def _mean_by_group(
|
|
| 30 |
values: torch.Tensor, groups: torch.Tensor, sizes: torch.Tensor
|
| 31 |
) -> torch.Tensor:
|
| 32 |
"""Per-group means; ``sizes`` holds the (exact, integer) group sizes."""
|
| 33 |
-
|
| 34 |
-
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
class EdgeNetwork(nn.Module):
|
|
|
|
| 15 |
def _sum_by_group(
|
| 16 |
values: torch.Tensor, groups: torch.Tensor, count: int
|
| 17 |
) -> torch.Tensor:
|
| 18 |
+
# Sums accumulate in at least float32, also under bfloat16 autocast.
|
| 19 |
+
dtype = torch.promote_types(values.dtype, torch.float32)
|
| 20 |
+
result = values.new_zeros((count, values.shape[1]), dtype=dtype)
|
| 21 |
+
return result.index_add_(0, groups, values.to(dtype))
|
| 22 |
|
| 23 |
|
| 24 |
def _gather(values: torch.Tensor, index: torch.Tensor) -> torch.Tensor:
|
|
|
|
| 32 |
values: torch.Tensor, groups: torch.Tensor, sizes: torch.Tensor
|
| 33 |
) -> torch.Tensor:
|
| 34 |
"""Per-group means; ``sizes`` holds the (exact, integer) group sizes."""
|
| 35 |
+
# ``shape[0]`` stays symbolic under torch.compile (no recompile per size).
|
| 36 |
+
sums = _sum_by_group(values, groups, sizes.shape[0])
|
| 37 |
+
return sums / sizes.to(sums.dtype)[:, None].clamp_min(1)
|
| 38 |
|
| 39 |
|
| 40 |
class EdgeNetwork(nn.Module):
|
|
@@ -57,6 +57,21 @@ def _state_dict(model: nn.Module) -> dict[str, Any]:
|
|
| 57 |
}
|
| 58 |
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
def _rng_state() -> dict[str, Any]:
|
| 61 |
state: dict[str, Any] = {
|
| 62 |
"python": random.getstate(),
|
|
@@ -151,7 +166,7 @@ class CheckpointManager:
|
|
| 151 |
"epoch": epoch,
|
| 152 |
"global_step": int(trainer_state.global_step),
|
| 153 |
"model_state_dict": _state_dict(model),
|
| 154 |
-
"optimizer_state_dict":
|
| 155 |
if optimizer is not None
|
| 156 |
else None,
|
| 157 |
"scheduler_state_dict": scheduler.state_dict()
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
|
| 60 |
+
def _portable_optimizer_state(optimizer: torch.optim.Optimizer) -> dict[str, Any]:
|
| 61 |
+
"""Optimizer state without CUDA-graph settings (tensor learning rates and
|
| 62 |
+
``capturable``), so any training mode can resume from it."""
|
| 63 |
+
state = optimizer.state_dict()
|
| 64 |
+
groups = []
|
| 65 |
+
for group in state["param_groups"]:
|
| 66 |
+
group = dict(group)
|
| 67 |
+
if isinstance(group.get("lr"), torch.Tensor):
|
| 68 |
+
group["lr"] = float(group["lr"])
|
| 69 |
+
if "capturable" in group:
|
| 70 |
+
group["capturable"] = False
|
| 71 |
+
groups.append(group)
|
| 72 |
+
return {**state, "param_groups": groups}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
def _rng_state() -> dict[str, Any]:
|
| 76 |
state: dict[str, Any] = {
|
| 77 |
"python": random.getstate(),
|
|
|
|
| 166 |
"epoch": epoch,
|
| 167 |
"global_step": int(trainer_state.global_step),
|
| 168 |
"model_state_dict": _state_dict(model),
|
| 169 |
+
"optimizer_state_dict": _portable_optimizer_state(optimizer)
|
| 170 |
if optimizer is not None
|
| 171 |
else None,
|
| 172 |
"scheduler_state_dict": scheduler.state_dict()
|
|
@@ -9,7 +9,7 @@ captures serve a whole epoch.
|
|
| 9 |
|
| 10 |
Results agree with eager training to floating-point rounding, not bitwise:
|
| 11 |
matrix multiplications see padded row counts and the optimizer runs in its
|
| 12 |
-
``capturable`` form.
|
| 13 |
"""
|
| 14 |
|
| 15 |
from __future__ import annotations
|
|
@@ -30,15 +30,31 @@ class _Capture:
|
|
| 30 |
buffers: StaticGraphBatch
|
| 31 |
loss: torch.Tensor
|
| 32 |
logits: torch.Tensor
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
|
| 36 |
class CudaGraphTrainStep:
|
| 37 |
"""Replay captured training steps for graph batches on a CUDA device.
|
| 38 |
|
| 39 |
-
The model must not synchronize with the host. A step is captured
|
| 40 |
-
``(graphs, nodes, edges)``
|
| 41 |
-
rate changes, because captured kernels hold the rate as a constant.
|
| 42 |
"""
|
| 43 |
|
| 44 |
def __init__(
|
|
@@ -53,8 +69,7 @@ class CudaGraphTrainStep:
|
|
| 53 |
) -> None:
|
| 54 |
if not isinstance(optimizer, (torch.optim.Adam, torch.optim.AdamW)):
|
| 55 |
raise ValueError("CUDA graph training supports the Adam optimizers")
|
| 56 |
-
|
| 57 |
-
group["capturable"] = True
|
| 58 |
self.model, self.task, self.optimizer = model, task, optimizer
|
| 59 |
self.forward = forward
|
| 60 |
self.node_multiple, self.edge_multiple = node_multiple, edge_multiple
|
|
@@ -64,13 +79,9 @@ class CudaGraphTrainStep:
|
|
| 64 |
def supports(batch: Any) -> bool:
|
| 65 |
return isinstance(batch, GraphBatch) and batch.graph.node_features.is_cuda
|
| 66 |
|
| 67 |
-
def
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def _capture(
|
| 71 |
-
self, key: tuple[int, int, int], batch: GraphBatch, rates: tuple[float, ...]
|
| 72 |
-
) -> _Capture:
|
| 73 |
-
self._captures.pop(key, None)
|
| 74 |
buffers = StaticGraphBatch(batch, key)
|
| 75 |
buffers.load(batch)
|
| 76 |
# Gradients allocated during capture belong to this graph's pool.
|
|
@@ -83,7 +94,7 @@ class CudaGraphTrainStep:
|
|
| 83 |
loss = self.task.loss(logits, buffers.batch)
|
| 84 |
loss.backward()
|
| 85 |
self.optimizer.step()
|
| 86 |
-
capture = _Capture(graph, buffers, loss, logits
|
| 87 |
self._captures[key] = capture
|
| 88 |
return capture
|
| 89 |
|
|
@@ -92,10 +103,9 @@ class CudaGraphTrainStep:
|
|
| 92 |
key = graph_batch_capacity(
|
| 93 |
batch, node_multiple=self.node_multiple, edge_multiple=self.edge_multiple
|
| 94 |
)
|
| 95 |
-
rates = self._learning_rates()
|
| 96 |
capture = self._captures.get(key)
|
| 97 |
-
if capture is None
|
| 98 |
-
capture = self._capture(key, batch
|
| 99 |
capture.buffers.load(batch)
|
| 100 |
capture.graph.replay()
|
| 101 |
return capture.loss.detach().clone(), capture.logits.detach().clone()
|
|
|
|
| 9 |
|
| 10 |
Results agree with eager training to floating-point rounding, not bitwise:
|
| 11 |
matrix multiplications see padded row counts and the optimizer runs in its
|
| 12 |
+
``capturable`` form with device-tensor learning rates.
|
| 13 |
"""
|
| 14 |
|
| 15 |
from __future__ import annotations
|
|
|
|
| 30 |
buffers: StaticGraphBatch
|
| 31 |
loss: torch.Tensor
|
| 32 |
logits: torch.Tensor
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def make_capturable(optimizer: torch.optim.Optimizer) -> None:
|
| 36 |
+
"""Prepare an Adam optimizer for capture (idempotent).
|
| 37 |
+
|
| 38 |
+
Learning rates become device tensors, which schedulers update in place,
|
| 39 |
+
so captured steps follow the schedule without being captured again.
|
| 40 |
+
Optimizer step counters move to the parameters' device.
|
| 41 |
+
"""
|
| 42 |
+
for group in optimizer.param_groups:
|
| 43 |
+
device = group["params"][0].device
|
| 44 |
+
group["capturable"] = True
|
| 45 |
+
if not isinstance(group["lr"], torch.Tensor):
|
| 46 |
+
group["lr"] = torch.tensor(float(group["lr"]), device=device)
|
| 47 |
+
for parameter in group["params"]:
|
| 48 |
+
state = optimizer.state.get(parameter, {})
|
| 49 |
+
if isinstance(state.get("step"), torch.Tensor):
|
| 50 |
+
state["step"] = state["step"].to(device)
|
| 51 |
|
| 52 |
|
| 53 |
class CudaGraphTrainStep:
|
| 54 |
"""Replay captured training steps for graph batches on a CUDA device.
|
| 55 |
|
| 56 |
+
The model must not synchronize with the host. A step is captured once per
|
| 57 |
+
``(graphs, nodes, edges)`` capacity bucket.
|
|
|
|
| 58 |
"""
|
| 59 |
|
| 60 |
def __init__(
|
|
|
|
| 69 |
) -> None:
|
| 70 |
if not isinstance(optimizer, (torch.optim.Adam, torch.optim.AdamW)):
|
| 71 |
raise ValueError("CUDA graph training supports the Adam optimizers")
|
| 72 |
+
make_capturable(optimizer)
|
|
|
|
| 73 |
self.model, self.task, self.optimizer = model, task, optimizer
|
| 74 |
self.forward = forward
|
| 75 |
self.node_multiple, self.edge_multiple = node_multiple, edge_multiple
|
|
|
|
| 79 |
def supports(batch: Any) -> bool:
|
| 80 |
return isinstance(batch, GraphBatch) and batch.graph.node_features.is_cuda
|
| 81 |
|
| 82 |
+
def _capture(self, key: tuple[int, int, int], batch: GraphBatch) -> _Capture:
|
| 83 |
+
# A restored checkpoint may have replaced the optimizer settings.
|
| 84 |
+
make_capturable(self.optimizer)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
buffers = StaticGraphBatch(batch, key)
|
| 86 |
buffers.load(batch)
|
| 87 |
# Gradients allocated during capture belong to this graph's pool.
|
|
|
|
| 94 |
loss = self.task.loss(logits, buffers.batch)
|
| 95 |
loss.backward()
|
| 96 |
self.optimizer.step()
|
| 97 |
+
capture = _Capture(graph, buffers, loss, logits)
|
| 98 |
self._captures[key] = capture
|
| 99 |
return capture
|
| 100 |
|
|
|
|
| 103 |
key = graph_batch_capacity(
|
| 104 |
batch, node_multiple=self.node_multiple, edge_multiple=self.edge_multiple
|
| 105 |
)
|
|
|
|
| 106 |
capture = self._captures.get(key)
|
| 107 |
+
if capture is None:
|
| 108 |
+
capture = self._capture(key, batch)
|
| 109 |
capture.buffers.load(batch)
|
| 110 |
capture.graph.replay()
|
| 111 |
return capture.loss.detach().clone(), capture.logits.detach().clone()
|
|
@@ -33,8 +33,22 @@ CUDA_GRAPH_WARMUP_STEPS = 3
|
|
| 33 |
_POSITIONAL_ARGUMENTS: dict[type, int] = {}
|
| 34 |
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
def _positional_arguments(model: nn.Module) -> int:
|
| 37 |
"""Number of positional ``forward`` parameters, cached per model class."""
|
|
|
|
| 38 |
cached = _POSITIONAL_ARGUMENTS.get(type(model))
|
| 39 |
if cached is not None:
|
| 40 |
return cached
|
|
@@ -99,9 +113,13 @@ class Trainer:
|
|
| 99 |
scheduler_step: str = "epoch",
|
| 100 |
distributed_context: DistributedContext | None = None,
|
| 101 |
cuda_graphs: bool = False,
|
|
|
|
|
|
|
| 102 |
) -> None:
|
| 103 |
if scheduler_step not in {"epoch", "validation"}:
|
| 104 |
raise ValueError("scheduler_step must be 'epoch' or 'validation'")
|
|
|
|
|
|
|
| 105 |
self.distributed_context = (
|
| 106 |
distributed_context or DistributedContext.single_process(device)
|
| 107 |
)
|
|
@@ -113,6 +131,13 @@ class Trainer:
|
|
| 113 |
self.early_stopping = early_stopping
|
| 114 |
self.scheduler_step = scheduler_step
|
| 115 |
self.state = TrainerState()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
self._graph_step = None
|
| 117 |
if cuda_graphs:
|
| 118 |
if self.distributed_context.enabled:
|
|
@@ -122,9 +147,19 @@ class Trainer:
|
|
| 122 |
from .cuda_graphs import CudaGraphTrainStep
|
| 123 |
|
| 124 |
self._graph_step = CudaGraphTrainStep(
|
| 125 |
-
self.model, task, optimizer,
|
| 126 |
)
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
def _step(self, batch: Any) -> tuple[torch.Tensor, torch.Tensor]:
|
| 129 |
"""One optimizer step; returns the detached loss and logits.
|
| 130 |
|
|
@@ -132,7 +167,7 @@ class Trainer:
|
|
| 132 |
batch while this one runs.
|
| 133 |
"""
|
| 134 |
self.optimizer.zero_grad(set_to_none=True)
|
| 135 |
-
logits =
|
| 136 |
loss = self.task.loss(logits, batch)
|
| 137 |
loss.backward()
|
| 138 |
self.optimizer.step()
|
|
@@ -206,7 +241,7 @@ class Trainer:
|
|
| 206 |
with torch.inference_mode():
|
| 207 |
for batch in loader:
|
| 208 |
moved = batch.to(self.device, non_blocking=True)
|
| 209 |
-
outputs.append(
|
| 210 |
batches.append(_summary(moved))
|
| 211 |
if not outputs:
|
| 212 |
raise ValueError("cannot evaluate an empty loader")
|
|
|
|
| 33 |
_POSITIONAL_ARGUMENTS: dict[type, int] = {}
|
| 34 |
|
| 35 |
|
| 36 |
+
#: ``trainer.precision`` values and their autocast dtype (``None``: float32).
|
| 37 |
+
PRECISIONS = {"float32": None, "bfloat16": torch.bfloat16}
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _unwrapped(model: nn.Module) -> nn.Module:
|
| 41 |
+
"""The user module inside ``torch.compile`` and DDP wrappers."""
|
| 42 |
+
while True:
|
| 43 |
+
inner = getattr(model, "_orig_mod", None) or getattr(model, "module", None)
|
| 44 |
+
if not isinstance(inner, nn.Module):
|
| 45 |
+
return model
|
| 46 |
+
model = inner
|
| 47 |
+
|
| 48 |
+
|
| 49 |
def _positional_arguments(model: nn.Module) -> int:
|
| 50 |
"""Number of positional ``forward`` parameters, cached per model class."""
|
| 51 |
+
model = _unwrapped(model)
|
| 52 |
cached = _POSITIONAL_ARGUMENTS.get(type(model))
|
| 53 |
if cached is not None:
|
| 54 |
return cached
|
|
|
|
| 113 |
scheduler_step: str = "epoch",
|
| 114 |
distributed_context: DistributedContext | None = None,
|
| 115 |
cuda_graphs: bool = False,
|
| 116 |
+
compile_model: bool = False,
|
| 117 |
+
precision: str = "float32",
|
| 118 |
) -> None:
|
| 119 |
if scheduler_step not in {"epoch", "validation"}:
|
| 120 |
raise ValueError("scheduler_step must be 'epoch' or 'validation'")
|
| 121 |
+
if precision not in PRECISIONS:
|
| 122 |
+
raise ValueError(f"precision must be one of {sorted(PRECISIONS)}")
|
| 123 |
self.distributed_context = (
|
| 124 |
distributed_context or DistributedContext.single_process(device)
|
| 125 |
)
|
|
|
|
| 131 |
self.early_stopping = early_stopping
|
| 132 |
self.scheduler_step = scheduler_step
|
| 133 |
self.state = TrainerState()
|
| 134 |
+
self.precision = precision
|
| 135 |
+
self._autocast_dtype = PRECISIONS[precision]
|
| 136 |
+
# ``self.model`` stays the eager module for checkpoints and callers;
|
| 137 |
+
# forward passes go through the compiled wrapper when requested.
|
| 138 |
+
self._forward_module = (
|
| 139 |
+
torch.compile(self.model, dynamic=True) if compile_model else self.model
|
| 140 |
+
)
|
| 141 |
self._graph_step = None
|
| 142 |
if cuda_graphs:
|
| 143 |
if self.distributed_context.enabled:
|
|
|
|
| 147 |
from .cuda_graphs import CudaGraphTrainStep
|
| 148 |
|
| 149 |
self._graph_step = CudaGraphTrainStep(
|
| 150 |
+
self.model, task, optimizer, lambda _model, batch: self._forward(batch)
|
| 151 |
)
|
| 152 |
|
| 153 |
+
def _forward(self, batch: Any) -> torch.Tensor:
|
| 154 |
+
"""Model logits, in float32 even when computed under autocast."""
|
| 155 |
+
if self._autocast_dtype is None:
|
| 156 |
+
return _model_forward(self._forward_module, batch)
|
| 157 |
+
with torch.autocast(
|
| 158 |
+
device_type=torch.device(self.device).type, dtype=self._autocast_dtype
|
| 159 |
+
):
|
| 160 |
+
logits = _model_forward(self._forward_module, batch)
|
| 161 |
+
return logits.float()
|
| 162 |
+
|
| 163 |
def _step(self, batch: Any) -> tuple[torch.Tensor, torch.Tensor]:
|
| 164 |
"""One optimizer step; returns the detached loss and logits.
|
| 165 |
|
|
|
|
| 167 |
batch while this one runs.
|
| 168 |
"""
|
| 169 |
self.optimizer.zero_grad(set_to_none=True)
|
| 170 |
+
logits = self._forward(batch)
|
| 171 |
loss = self.task.loss(logits, batch)
|
| 172 |
loss.backward()
|
| 173 |
self.optimizer.step()
|
|
|
|
| 241 |
with torch.inference_mode():
|
| 242 |
for batch in loader:
|
| 243 |
moved = batch.to(self.device, non_blocking=True)
|
| 244 |
+
outputs.append(self._forward(moved).detach())
|
| 245 |
batches.append(_summary(moved))
|
| 246 |
if not outputs:
|
| 247 |
raise ValueError("cannot evaluate an empty loader")
|
|
@@ -14,10 +14,10 @@ from pathlib import Path
|
|
| 14 |
|
| 15 |
import numpy as np
|
| 16 |
import torch
|
|
|
|
| 17 |
|
| 18 |
from gnn4colliders.compat import load_legacy_checkpoint
|
| 19 |
from gnn4colliders.data import GraphDataLoader, GraphDataset
|
| 20 |
-
from gnn4colliders.graphs.preparation import build_graph_samples, open_event_source
|
| 21 |
from gnn4colliders.models.root_gnn import (
|
| 22 |
EdgeNetwork,
|
| 23 |
FineTunedEdgeNetwork,
|
|
|
|
| 14 |
|
| 15 |
import numpy as np
|
| 16 |
import torch
|
| 17 |
+
from gnn4colliders.graphs.preparation import build_graph_samples, open_event_source
|
| 18 |
|
| 19 |
from gnn4colliders.compat import load_legacy_checkpoint
|
| 20 |
from gnn4colliders.data import GraphDataLoader, GraphDataset
|
|
|
|
| 21 |
from gnn4colliders.models.root_gnn import (
|
| 22 |
EdgeNetwork,
|
| 23 |
FineTunedEdgeNetwork,
|
|
@@ -123,3 +123,62 @@ def test_cuda_graph_steps_match_eager_gradients():
|
|
| 123 |
assert logits.shape == (16, 3)
|
| 124 |
for left, right in zip(eager.parameters(), graphed.parameters()):
|
| 125 |
torch.testing.assert_close(right.grad, left.grad, rtol=1e-4, atol=1e-6)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
assert logits.shape == (16, 3)
|
| 124 |
for left, right in zip(eager.parameters(), graphed.parameters()):
|
| 125 |
torch.testing.assert_close(right.grad, left.grad, rtol=1e-4, atol=1e-6)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def test_bfloat16_autocast_trains_with_float32_logits_and_weights():
|
| 129 |
+
events = _events(seed=6)
|
| 130 |
+
trainer = _trainer(events, precision="bfloat16")
|
| 131 |
+
batch = next(iter(EventLoader(events, collate_graph_batch, 8)))
|
| 132 |
+
loss, logits = trainer._step(batch)
|
| 133 |
+
assert logits.dtype == torch.float32 and torch.isfinite(loss)
|
| 134 |
+
assert all(p.dtype == torch.float32 for p in trainer.model.parameters())
|
| 135 |
+
reference = _trainer(events)._forward(batch)
|
| 136 |
+
torch.testing.assert_close(
|
| 137 |
+
_trainer(events, precision="bfloat16")._forward(batch),
|
| 138 |
+
reference,
|
| 139 |
+
rtol=5e-2,
|
| 140 |
+
atol=5e-2,
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def test_unknown_precision_is_rejected():
|
| 145 |
+
with pytest.raises(ValueError, match="precision"):
|
| 146 |
+
_trainer(_events(), precision="float16")
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@pytest.mark.gpu
|
| 150 |
+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA unavailable")
|
| 151 |
+
def test_compiled_graph_steps_with_bfloat16_track_eager_training():
|
| 152 |
+
events = _events(96, seed=7)
|
| 153 |
+
loader = EventLoader(events, collate_graph_batch, 16)
|
| 154 |
+
|
| 155 |
+
def run(**options):
|
| 156 |
+
trainer = _trainer(events, device="cuda", **options)
|
| 157 |
+
return [trainer.train_epoch(loader).loss for _ in range(3)]
|
| 158 |
+
|
| 159 |
+
eager = run()
|
| 160 |
+
fast = run(compile_model=True, cuda_graphs=True, precision="bfloat16")
|
| 161 |
+
assert fast == pytest.approx(eager, rel=5e-2)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
@pytest.mark.gpu
|
| 165 |
+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA unavailable")
|
| 166 |
+
def test_cuda_graph_steps_follow_the_learning_rate_schedule(tmp_path):
|
| 167 |
+
from gnn4colliders.training import CheckpointManager
|
| 168 |
+
|
| 169 |
+
events = _events(96, seed=8)
|
| 170 |
+
loader = EventLoader(events, collate_graph_batch, 16)
|
| 171 |
+
trainer = _trainer(events, device="cuda", cuda_graphs=True)
|
| 172 |
+
trainer.train_epoch(loader) # warmup steps, then captured replays
|
| 173 |
+
# A zero rate must reach the already captured steps.
|
| 174 |
+
torch.optim.lr_scheduler.ExponentialLR(trainer.optimizer, gamma=0.0).step()
|
| 175 |
+
before = [p.detach().clone() for p in trainer.model.parameters()]
|
| 176 |
+
trainer.train_epoch(loader)
|
| 177 |
+
for old, new in zip(before, trainer.model.parameters()):
|
| 178 |
+
assert torch.equal(old, new)
|
| 179 |
+
# Checkpoints stay loadable by eager training on any device.
|
| 180 |
+
path = CheckpointManager(tmp_path).save(
|
| 181 |
+
model=trainer.model, trainer_state=trainer.state, optimizer=trainer.optimizer
|
| 182 |
+
)
|
| 183 |
+
groups = CheckpointManager.load(path)["optimizer_state_dict"]["param_groups"]
|
| 184 |
+
assert all(type(g["lr"]) is float and not g["capturable"] for g in groups)
|
|
The diff for this file is too large to render.
See raw diff
|
|
|