ho22joshua commited on
Commit
be26097
·
1 Parent(s): fe85bec

feat: add explicit legacy compatibility adapters

Browse files
docs/compatibility.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Compatibility boundary
2
+
3
+ The package's canonical APIs use named `EventMetadata` fields and the version
4
+ 1 checkpoint schema. Compatibility is explicit and one-way: old artifacts are
5
+ adapted into the new representation and are never rewritten implicitly.
6
+
7
+ | Artifact or behavior | Supported | Boundary | Notes |
8
+ | --- | :---: | --- | --- |
9
+ | New checkpoint (`schema_version: 1`) | yes | `CheckpointManager` | Full model/lifecycle resume when state is present |
10
+ | Legacy `model_epoch_N.pt` checkpoint | yes | `gnn4colliders.compat.load_legacy_checkpoint` | Model state and active early-stop fields are adapted |
11
+ | Legacy DDP/compiled prefixes | yes | `normalize_legacy_state_dict_keys` | Supports `module.` and `_orig_mod.` |
12
+ | Legacy ROOT-GNN classifier name | yes | `map_legacy_edge_network_state_dict` | Maps `classify` to `classifier` |
13
+ | Legacy optimizer state | partial | legacy checkpoint adapter | Loaded when present; scheduler state is not available in the historical format |
14
+ | Legacy scheduler state | no | — | Historical checkpoints do not carry a supported scheduler state |
15
+ | Positional tracking rows | yes, at ingestion boundary | `EventMetadata.from_legacy_tracking` | Exactly `tracking[0] = fold`, `tracking[1] = weight`; shorter rows fail |
16
+ | Generic/unknown tracking layouts | no | — | The package does not guess historical column meanings |
17
+ | Modern NPZ output | yes | `inference.write_npz` | Named fields: `sample_id`, `logits`, `scores`, `predictions`, and available metadata |
18
+ | Historical `tracking_info` NPZ output | no | — | No active consumer remains; positional output is intentionally unsupported |
19
+ | Legacy YAML `module`/`class`/`args` | compatibility only | configuration boundary | Accepted only where the semantic factory can safely interpret it; new configs use semantic model names |
20
+
21
+ The frozen `legacy/` tree remains available to parity tests and historical
22
+ investigation. Production modules do not import executable code from it.
src/gnn4colliders/compat/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Explicit adapters for artifacts from the frozen legacy implementation."""
2
+
3
+ from .checkpoint import (
4
+ LEGACY_CHECKPOINT_SCHEMA_VERSION,
5
+ load_legacy_checkpoint,
6
+ map_legacy_edge_network_state_dict,
7
+ normalize_legacy_state_dict_keys,
8
+ )
9
+ from .metadata import event_metadata_from_legacy_tracking
10
+
11
+ __all__ = [
12
+ "LEGACY_CHECKPOINT_SCHEMA_VERSION",
13
+ "event_metadata_from_legacy_tracking",
14
+ "load_legacy_checkpoint",
15
+ "map_legacy_edge_network_state_dict",
16
+ "normalize_legacy_state_dict_keys",
17
+ ]
src/gnn4colliders/compat/checkpoint.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Checkpoint adapters for the supported historical ROOT-GNN artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import torch
10
+
11
+ LEGACY_CHECKPOINT_SCHEMA_VERSION = 0
12
+
13
+
14
+ def normalize_legacy_state_dict_keys(state: Mapping[str, Any]) -> dict[str, Any]:
15
+ """Remove the supported DDP and compilation wrappers deterministically."""
16
+ return {
17
+ key.removeprefix("module.").removeprefix("_orig_mod."): value
18
+ for key, value in state.items()
19
+ }
20
+
21
+
22
+ def map_legacy_edge_network_state_dict(
23
+ state: Mapping[str, Any],
24
+ ) -> dict[str, Any]:
25
+ """Map historical ``classify`` keys to the modern ``classifier`` name."""
26
+ normalized = normalize_legacy_state_dict_keys(state)
27
+ return {
28
+ key.replace(".classify.", ".classifier.").replace(
29
+ "classify.", "classifier."
30
+ ): value
31
+ for key, value in normalized.items()
32
+ }
33
+
34
+
35
+ def load_legacy_checkpoint(
36
+ source: str | Path | Mapping[str, Any], *, map_location: Any = "cpu"
37
+ ) -> dict[str, Any]:
38
+ """Adapt an active ``model_epoch_N.pt`` payload without rewriting it."""
39
+ payload = (
40
+ torch.load(source, map_location=map_location, weights_only=False)
41
+ if not isinstance(source, Mapping)
42
+ else dict(source)
43
+ )
44
+ if not isinstance(payload, Mapping) or "model_state_dict" not in payload:
45
+ raise ValueError("not a supported legacy ROOT-GNN checkpoint")
46
+ early = payload.get("early_stop")
47
+ if isinstance(early, Mapping):
48
+ early = {
49
+ "patience": early.get("patience", 15),
50
+ "min_delta": early.get("threshold", 1e-8),
51
+ "mode": early.get("mode", "min"),
52
+ "best": early.get("current_best", float("inf")),
53
+ "num_bad_epochs": early.get("count", 0),
54
+ "should_stop": early.get("should_stop", False),
55
+ }
56
+ epoch = int(payload.get("epoch", -1))
57
+ return {
58
+ "schema_version": LEGACY_CHECKPOINT_SCHEMA_VERSION,
59
+ "legacy": True,
60
+ "epoch": epoch,
61
+ "global_step": 0,
62
+ "model_state_dict": normalize_legacy_state_dict_keys(
63
+ payload["model_state_dict"]
64
+ ),
65
+ "optimizer_state_dict": payload.get("optimizer_state_dict"),
66
+ "scheduler_state_dict": None,
67
+ "early_stopping_state": early,
68
+ "trainer_state": {"epoch": epoch, "global_step": 0},
69
+ "model_config": None,
70
+ "task_config": None,
71
+ "metadata": {"legacy_format": True},
72
+ }
src/gnn4colliders/compat/metadata.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Adapters for the historical positional event metadata layout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import Any
7
+
8
+ from gnn4colliders.data.metadata import EventMetadata
9
+
10
+
11
+ def event_metadata_from_legacy_tracking(
12
+ tracking_row: Sequence[Any],
13
+ *,
14
+ sample_id: str,
15
+ extra: Mapping[str, Any] | None = None,
16
+ ) -> EventMetadata:
17
+ """Convert the only supported legacy layout into named metadata.
18
+
19
+ The historical contract is strictly ``tracking[0] = fold`` and
20
+ ``tracking[1] = weight``. Additional historical columns are not
21
+ interpreted or propagated.
22
+ """
23
+ if len(tracking_row) < 2:
24
+ raise ValueError(
25
+ "legacy tracking must contain at least two values: fold and weight"
26
+ )
27
+ return EventMetadata(
28
+ fold=int(tracking_row[0]),
29
+ weight=float(tracking_row[1]),
30
+ sample_id=sample_id,
31
+ extra=dict(extra or {}),
32
+ )
src/gnn4colliders/config/factories.py CHANGED
@@ -7,6 +7,7 @@ from typing import Any
7
 
8
  import torch
9
 
 
10
  from gnn4colliders.models.root_gnn import EdgeNetwork, FineTunedEdgeNetwork
11
  from gnn4colliders.tasks import BinaryClassificationTask, MulticlassClassificationTask
12
  from gnn4colliders.training import (
@@ -101,7 +102,11 @@ def build_task(config: Mapping[str, Any]) -> Any:
101
 
102
 
103
  def build_trainer(
104
- config: Mapping[str, Any], *, model: torch.nn.Module, task: Any
 
 
 
 
105
  ) -> Trainer:
106
  optimizer_config = _get(config, "optimizer", {}) or {}
107
  optimizer = build_optimizer(model, **dict(optimizer_config))
@@ -136,4 +141,5 @@ def build_trainer(
136
  device=str(_get(config, "device", "cpu")),
137
  early_stopping=early,
138
  scheduler_step=scheduler_step,
 
139
  )
 
7
 
8
  import torch
9
 
10
+ from gnn4colliders.distributed import DistributedContext
11
  from gnn4colliders.models.root_gnn import EdgeNetwork, FineTunedEdgeNetwork
12
  from gnn4colliders.tasks import BinaryClassificationTask, MulticlassClassificationTask
13
  from gnn4colliders.training import (
 
102
 
103
 
104
  def build_trainer(
105
+ config: Mapping[str, Any],
106
+ *,
107
+ model: torch.nn.Module,
108
+ task: Any,
109
+ distributed_context: DistributedContext | None = None,
110
  ) -> Trainer:
111
  optimizer_config = _get(config, "optimizer", {}) or {}
112
  optimizer = build_optimizer(model, **dict(optimizer_config))
 
141
  device=str(_get(config, "device", "cpu")),
142
  early_stopping=early,
143
  scheduler_step=scheduler_step,
144
+ distributed_context=distributed_context,
145
  )
tests/unit/compat/__init__.py ADDED
File without changes
tests/unit/compat/test_adapters.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ import torch
3
+
4
+ from gnn4colliders.compat import (
5
+ event_metadata_from_legacy_tracking,
6
+ map_legacy_edge_network_state_dict,
7
+ )
8
+ from gnn4colliders.data import EventMetadata
9
+
10
+
11
+ def test_legacy_tracking_is_converted_to_named_metadata():
12
+ metadata = event_metadata_from_legacy_tracking(
13
+ [3.0, -2.5, 99.0], sample_id="event:7"
14
+ )
15
+ assert metadata == EventMetadata(fold=3, weight=-2.5, sample_id="event:7")
16
+
17
+
18
+ def test_legacy_tracking_rejects_ambiguous_short_rows():
19
+ with pytest.raises(ValueError, match="fold and weight"):
20
+ EventMetadata.from_legacy_tracking([3.0], sample_id="event:7")
21
+
22
+
23
+ def test_legacy_state_dict_mapping_is_canonical_and_deterministic():
24
+ state = {
25
+ "module._orig_mod.encoder.weight": torch.ones(2, 2),
26
+ "module._orig_mod.classify.bias": torch.zeros(1),
27
+ }
28
+ mapped = map_legacy_edge_network_state_dict(state)
29
+ assert set(mapped) == {"encoder.weight", "classifier.bias"}