eae-runtime 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. eae_runtime/__init__.py +80 -0
  2. eae_runtime/_version.py +24 -0
  3. eae_runtime/adjoint.py +128 -0
  4. eae_runtime/backend.py +52 -0
  5. eae_runtime/blocks.py +69 -0
  6. eae_runtime/boundary_store.py +65 -0
  7. eae_runtime/config.py +48 -0
  8. eae_runtime/contrib/__init__.py +25 -0
  9. eae_runtime/contrib/transformer_blocks.py +259 -0
  10. eae_runtime/events.py +84 -0
  11. eae_runtime/forward_executor.py +35 -0
  12. eae_runtime/logging_utils.py +41 -0
  13. eae_runtime/memory.py +175 -0
  14. eae_runtime/passes/__init__.py +18 -0
  15. eae_runtime/passes/base.py +31 -0
  16. eae_runtime/passes/clip.py +25 -0
  17. eae_runtime/passes/logging_pass.py +29 -0
  18. eae_runtime/passes/quantize.py +55 -0
  19. eae_runtime/passes/regularization.py +47 -0
  20. eae_runtime/passes/synthetic_gradient.py +67 -0
  21. eae_runtime/pipeline.py +58 -0
  22. eae_runtime/profiler.py +47 -0
  23. eae_runtime/py.typed +0 -0
  24. eae_runtime/reconstruction.py +89 -0
  25. eae_runtime/runtime.py +180 -0
  26. eae_runtime/schedulers/__init__.py +38 -0
  27. eae_runtime/schedulers/async_scheduler.py +65 -0
  28. eae_runtime/schedulers/base.py +87 -0
  29. eae_runtime/schedulers/distributed.py +78 -0
  30. eae_runtime/schedulers/pipeline_scheduler.py +67 -0
  31. eae_runtime/schedulers/sequential.py +27 -0
  32. eae_runtime-0.1.0.dist-info/METADATA +229 -0
  33. eae_runtime-0.1.0.dist-info/RECORD +38 -0
  34. eae_runtime-0.1.0.dist-info/WHEEL +5 -0
  35. eae_runtime-0.1.0.dist-info/licenses/LICENSE +13 -0
  36. eae_runtime-0.1.0.dist-info/scm_file_list.json +59 -0
  37. eae_runtime-0.1.0.dist-info/scm_version.json +8 -0
  38. eae_runtime-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,80 @@
1
+ """
2
+ EAE Runtime: a standalone, programmable, block-local reverse-mode execution
3
+ engine that sits *above* PyTorch and orchestrates many local autograd
4
+ computations, instead of replacing PyTorch's autodiff engine.
5
+
6
+ Quick start:
7
+
8
+ import torch.nn as nn
9
+ from eae_runtime import EAERuntime, RuntimeConfig
10
+ from eae_runtime.passes import ClipPass, GaussianNoisePass
11
+
12
+ model = nn.Sequential(
13
+ nn.Linear(64, 64), nn.ReLU(),
14
+ nn.Linear(64, 64), nn.ReLU(),
15
+ nn.Linear(64, 10),
16
+ )
17
+ optimizer = torch.optim.SGD(model.parameters(), lr=1e-2)
18
+ config = RuntimeConfig(
19
+ scheduler="sequential",
20
+ memory="pool",
21
+ backend="auto",
22
+ passes=[ClipPass(max_norm=1.0)],
23
+ )
24
+ runtime = EAERuntime(model, optimizer, config)
25
+ loss = runtime.train_step(x, lambda out: nn.functional.mse_loss(out, target))
26
+ """
27
+
28
+ from .adjoint import AdjointState
29
+ from .backend import BackendManager
30
+ from .blocks import BlockDecomposer, EAEBlock
31
+ from .boundary_store import BoundaryStore
32
+ from .config import RuntimeConfig
33
+ from .events import Event, EventBus, EventType
34
+ from .forward_executor import ForwardExecutor
35
+ from .memory import MemoryManager, MemoryPolicy, NullMemoryPolicy, PoolMemoryPolicy
36
+ from .pipeline import AdjointPipeline, PassManager
37
+ from .profiler import Profiler
38
+ from .reconstruction import ReconstructionEngine
39
+ from .runtime import EAERuntime
40
+ from .schedulers import (
41
+ AsyncScheduler,
42
+ BaseScheduler,
43
+ DistributedScheduler,
44
+ PipelineScheduler,
45
+ SequentialScheduler,
46
+ build_scheduler,
47
+ )
48
+
49
+ try:
50
+ from ._version import version as __version__
51
+ except ImportError: # pragma: no cover - only hit in a non-built source tree
52
+ __version__ = "0.0.0+unknown"
53
+
54
+ __all__ = [
55
+ "AdjointState",
56
+ "BackendManager",
57
+ "BlockDecomposer",
58
+ "EAEBlock",
59
+ "BoundaryStore",
60
+ "RuntimeConfig",
61
+ "Event",
62
+ "EventBus",
63
+ "EventType",
64
+ "ForwardExecutor",
65
+ "MemoryManager",
66
+ "MemoryPolicy",
67
+ "NullMemoryPolicy",
68
+ "PoolMemoryPolicy",
69
+ "AdjointPipeline",
70
+ "PassManager",
71
+ "Profiler",
72
+ "ReconstructionEngine",
73
+ "EAERuntime",
74
+ "AsyncScheduler",
75
+ "BaseScheduler",
76
+ "DistributedScheduler",
77
+ "PipelineScheduler",
78
+ "SequentialScheduler",
79
+ "build_scheduler",
80
+ ]
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = 'g336fa5cb8'
eae_runtime/adjoint.py ADDED
@@ -0,0 +1,128 @@
1
+ """
2
+ AdjointState: the first-class runtime object that flows through the EAE runtime.
3
+
4
+ Design principle (see spec, "Explicit Adjoint States"):
5
+ The runtime never passes raw tensors internally. Everything flows through
6
+ AdjointState. Passes, schedulers and the reconstruction engine all read
7
+ and write AdjointState objects, never bare torch.Tensors.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import copy
13
+ from dataclasses import dataclass, field
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ import torch
17
+
18
+
19
+ @dataclass
20
+ class AdjointState:
21
+ """A gradient ("adjoint") flowing backward through the runtime, plus metadata.
22
+
23
+ Attributes:
24
+ tensor: the actual gradient tensor (d Loss / d activation).
25
+ layer_id: index of the block boundary this adjoint sits at (L, L-1, ..., 0).
26
+ block: human readable name of the block that produced/consumes this adjoint.
27
+ dtype: logical dtype this adjoint should be materialized at.
28
+ device: logical device this adjoint should be materialized at.
29
+ metadata: free-form dict for passes to stash information.
30
+ history: ordered list of pass names that have touched this adjoint,
31
+ for debugging / provenance / profiling.
32
+ """
33
+
34
+ tensor: torch.Tensor
35
+ layer_id: int
36
+ block: str = ""
37
+ dtype: Optional[torch.dtype] = None
38
+ device: Optional[torch.device] = None
39
+ metadata: Dict[str, Any] = field(default_factory=dict)
40
+ history: List[str] = field(default_factory=list)
41
+
42
+ def __post_init__(self):
43
+ if self.dtype is None:
44
+ self.dtype = self.tensor.dtype
45
+ if self.device is None:
46
+ self.device = self.tensor.device
47
+
48
+ # ------------------------------------------------------------------ #
49
+ # Rich adjoint API - users should almost never touch .tensor directly
50
+ # ------------------------------------------------------------------ #
51
+ def norm(self, p: float = 2.0) -> torch.Tensor:
52
+ """L-p norm of the underlying gradient tensor."""
53
+ return torch.linalg.vector_norm(self.tensor.float(), ord=p)
54
+
55
+ def statistics(self) -> Dict[str, float]:
56
+ """Cheap summary statistics, useful for logging/profiling passes."""
57
+ t = self.tensor.detach().float()
58
+ return {
59
+ "mean": t.mean().item(),
60
+ "std": t.std(unbiased=False).item() if t.numel() > 1 else 0.0,
61
+ "min": t.min().item(),
62
+ "max": t.max().item(),
63
+ "norm": torch.linalg.vector_norm(t).item(),
64
+ "numel": t.numel(),
65
+ "has_nan": bool(torch.isnan(t).any().item()),
66
+ "has_inf": bool(torch.isinf(t).any().item()),
67
+ }
68
+
69
+ def quantize(self, dtype: torch.dtype = torch.float16) -> "AdjointState":
70
+ """Return a new AdjointState with the tensor cast down to `dtype`."""
71
+ new = self.clone()
72
+ new.tensor = new.tensor.to(dtype)
73
+ new.dtype = dtype
74
+ return new
75
+
76
+ def dequantize(self, dtype: torch.dtype = torch.float32) -> "AdjointState":
77
+ new = self.clone()
78
+ new.tensor = new.tensor.to(dtype)
79
+ new.dtype = dtype
80
+ return new
81
+
82
+ def compress(self, ratio: float = 0.5) -> "AdjointState":
83
+ """Simple magnitude-pruning compression pass, kept out of the runtime
84
+ core on purpose - this is exactly the kind of thing a real Pass would
85
+ do; it lives here only as a convenience primitive on the adjoint."""
86
+ new = self.clone()
87
+ flat = new.tensor.flatten()
88
+ k = max(1, int(flat.numel() * (1.0 - ratio)))
89
+ if k < flat.numel():
90
+ threshold = flat.abs().kthvalue(flat.numel() - k + 1).values
91
+ mask = flat.abs() >= threshold
92
+ flat = flat * mask
93
+ new.tensor = flat.view_as(new.tensor)
94
+ return new
95
+
96
+ def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> "AdjointState":
97
+ new = self.clone()
98
+ new.tensor = new.tensor.to(device=device or new.device, dtype=dtype or new.dtype)
99
+ new.device = new.tensor.device
100
+ new.dtype = new.tensor.dtype
101
+ return new
102
+
103
+ def clone(self) -> "AdjointState":
104
+ return AdjointState(
105
+ tensor=self.tensor,
106
+ layer_id=self.layer_id,
107
+ block=self.block,
108
+ dtype=self.dtype,
109
+ device=self.device,
110
+ metadata=dict(self.metadata),
111
+ history=list(self.history),
112
+ )
113
+
114
+ def detach(self) -> "AdjointState":
115
+ new = self.clone()
116
+ new.tensor = new.tensor.detach()
117
+ return new
118
+
119
+ def record(self, pass_name: str) -> None:
120
+ """Append a pass name to this adjoint's provenance history."""
121
+ self.history.append(pass_name)
122
+
123
+ def __repr__(self) -> str: # pragma: no cover - cosmetic
124
+ return (
125
+ f"AdjointState(layer_id={self.layer_id}, block={self.block!r}, "
126
+ f"shape={tuple(self.tensor.shape)}, dtype={self.dtype}, "
127
+ f"device={self.device}, history={self.history})"
128
+ )
eae_runtime/backend.py ADDED
@@ -0,0 +1,52 @@
1
+ """
2
+ Backend Manager: responsible for choosing the execution backend. Blocks can
3
+ override backend selection by carrying a `.eae_backend` attribute naming a
4
+ preferred backend.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import contextlib
10
+ from typing import Optional
11
+
12
+ import torch
13
+
14
+
15
+ class BackendManager:
16
+ SUPPORTED = ("cpu", "cuda", "triton", "rocm", "auto")
17
+
18
+ def __init__(self, backend: str = "auto"):
19
+ if backend not in self.SUPPORTED:
20
+ raise ValueError(f"Unknown backend '{backend}', choices: {self.SUPPORTED}")
21
+ self.requested = backend
22
+ self.resolved = self._resolve(backend)
23
+
24
+ @staticmethod
25
+ def _resolve(backend: str) -> str:
26
+ if backend == "auto":
27
+ return "cuda" if torch.cuda.is_available() else "cpu"
28
+ if backend == "cuda" and not torch.cuda.is_available():
29
+ return "cpu" # graceful fallback rather than a hard failure
30
+ if backend in ("triton", "rocm"):
31
+ # Triton/ROCm kernels are launched *through* CUDA devices in this
32
+ # runtime; if no CUDA device is present we fall back to plain CPU
33
+ # execution so the same code runs everywhere.
34
+ return "cuda" if torch.cuda.is_available() else "cpu"
35
+ return backend
36
+
37
+ def device_for(self, block=None) -> torch.device:
38
+ override = getattr(block, "eae_backend", None)
39
+ backend = self._resolve(override) if override else self.resolved
40
+ return torch.device(backend if backend in ("cpu", "cuda") else "cpu")
41
+
42
+ @contextlib.contextmanager
43
+ def autocast(self, enabled: bool = False, dtype: Optional[torch.dtype] = None):
44
+ device_type = "cuda" if self.resolved == "cuda" else "cpu"
45
+ if not enabled:
46
+ yield
47
+ return
48
+ with torch.autocast(device_type=device_type, dtype=dtype or torch.float16, enabled=True):
49
+ yield
50
+
51
+ def __repr__(self) -> str: # pragma: no cover
52
+ return f"BackendManager(requested={self.requested!r}, resolved={self.resolved!r})"
eae_runtime/blocks.py ADDED
@@ -0,0 +1,69 @@
1
+ """
2
+ Block interface. The runtime reconstructs blocks independently - no block
3
+ should know anything about global training.
4
+
5
+ Per the spec's non-goals, we do NOT attempt automatic partitioning of
6
+ arbitrary nn.Module graphs. Manual / helper-based block specification is
7
+ sufficient: users hand us a `nn.Sequential` (or any list of `nn.Module`s
8
+ that compose linearly, e.g. repeated Transformer blocks) and we treat each
9
+ element as one reconstructable unit.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import List, Sequence
15
+
16
+ import torch.nn as nn
17
+
18
+
19
+ class EAEBlock(nn.Module):
20
+ """Optional marker base class for a block usable by the EAE runtime.
21
+
22
+ Subclassing this is *not* required - any `nn.Module` whose `forward`
23
+ is a pure function of its input and its own parameters works fine.
24
+ Subclass it when you want to be explicit about intent, or want the
25
+ stricter contract this class documents:
26
+
27
+ * forward(x) -> Tensor (single tensor in, single tensor out)
28
+ * parameters() -> Iterator[Tensor] (inherited from nn.Module)
29
+ * no reliance on global/mutable state outside the block
30
+ """
31
+
32
+ def forward(self, x): # pragma: no cover - documentation stub
33
+ raise NotImplementedError
34
+
35
+
36
+ class BlockDecomposer:
37
+ """Turns a user model into an ordered list of reconstructable blocks."""
38
+
39
+ @staticmethod
40
+ def from_sequential(seq: nn.Sequential) -> List[nn.Module]:
41
+ return list(seq.children())
42
+
43
+ @staticmethod
44
+ def from_list(modules: Sequence[nn.Module]) -> List[nn.Module]:
45
+ return list(modules)
46
+
47
+ @staticmethod
48
+ def decompose(model) -> List[nn.Module]:
49
+ """Best-effort dispatcher: nn.Sequential -> children, nn.ModuleList ->
50
+ children, list-like -> list, single nn.Module -> a one-block model.
51
+
52
+ `nn.ModuleList` is handled explicitly (not just via generic
53
+ `nn.Module` iteration) because it's the idiomatic way to hold a
54
+ repeated Transformer block stack, e.g.
55
+ `nn.ModuleList([TransformerBlock(...) for _ in range(depth)])`.
56
+ """
57
+ if isinstance(model, nn.Sequential):
58
+ return BlockDecomposer.from_sequential(model)
59
+ if isinstance(model, nn.ModuleList):
60
+ return list(model)
61
+ if isinstance(model, (list, tuple)):
62
+ return BlockDecomposer.from_list(model)
63
+ if isinstance(model, nn.Module):
64
+ return [model]
65
+ raise TypeError(
66
+ f"Cannot decompose object of type {type(model)} into EAE blocks. "
67
+ "Pass an nn.Sequential, an nn.ModuleList, a list of nn.Module, "
68
+ "or a single nn.Module."
69
+ )
@@ -0,0 +1,65 @@
1
+ """
2
+ Boundary Store: stores only x0, x1, ..., xL (block-boundary activations).
3
+ Nothing else - no internal activations, no autograd graph.
4
+
5
+ Supports CPU offload, pinned memory, optional compression and configurable
6
+ storage precision, all orthogonal to the reconstruction logic above it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Dict, Optional
12
+
13
+ import torch
14
+
15
+
16
+ class BoundaryStore:
17
+ def __init__(
18
+ self,
19
+ offload: bool = False,
20
+ precision: Optional[torch.dtype] = None,
21
+ pin_memory: bool = False,
22
+ ):
23
+ self.offload = offload
24
+ self.precision = precision
25
+ self.pin_memory = pin_memory and torch.cuda.is_available()
26
+ self._store: Dict[int, torch.Tensor] = {}
27
+ self._orig_dtype: Dict[int, torch.dtype] = {}
28
+ self._orig_device: Dict[int, torch.device] = {}
29
+
30
+ def put(self, idx: int, tensor: torch.Tensor) -> None:
31
+ t = tensor.detach()
32
+ self._orig_dtype[idx] = t.dtype
33
+ self._orig_device[idx] = t.device
34
+
35
+ if self.precision is not None:
36
+ t = t.to(self.precision)
37
+ if self.offload:
38
+ t = t.to("cpu")
39
+ if self.pin_memory:
40
+ t = t.pin_memory()
41
+ else:
42
+ t = t.clone() # own our copy, independent of caller's buffer
43
+ self._store[idx] = t
44
+
45
+ def get(self, idx: int, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> torch.Tensor:
46
+ if idx not in self._store:
47
+ raise KeyError(f"Boundary {idx} was never stored")
48
+ t = self._store[idx]
49
+ target_device = device if device is not None else self._orig_device[idx]
50
+ target_dtype = dtype if dtype is not None else self._orig_dtype[idx]
51
+ return t.to(device=target_device, dtype=target_dtype)
52
+
53
+ def __contains__(self, idx: int) -> bool:
54
+ return idx in self._store
55
+
56
+ def __len__(self) -> int:
57
+ return len(self._store)
58
+
59
+ def clear(self) -> None:
60
+ self._store.clear()
61
+ self._orig_dtype.clear()
62
+ self._orig_device.clear()
63
+
64
+ def memory_bytes(self) -> int:
65
+ return sum(t.element_size() * t.nelement() for t in self._store.values())
eae_runtime/config.py ADDED
@@ -0,0 +1,48 @@
1
+ """
2
+ Single configuration object for the runtime. No scattered options.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, List, Optional, Union
9
+
10
+ import torch
11
+
12
+
13
+ @dataclass
14
+ class RuntimeConfig:
15
+ # scheduler: name string ("sequential" | "async" | "pipeline" | "distributed")
16
+ # or a BaseScheduler instance for full user control.
17
+ scheduler: Union[str, Any] = "sequential"
18
+
19
+ # memory policy: name string ("pool" | "none") or a MemoryPolicy instance.
20
+ memory: Union[str, Any] = "pool"
21
+
22
+ # backend: name string ("cpu" | "cuda" | "triton" | "rocm") or a
23
+ # BackendManager instance.
24
+ backend: Union[str, Any] = "auto"
25
+
26
+ # ordered list of EAEPass instances forming the adjoint pipeline.
27
+ passes: List[Any] = field(default_factory=list)
28
+
29
+ # mixed precision compute dtype used during reconstruction (None = fp32).
30
+ compute_dtype: Optional[torch.dtype] = None
31
+
32
+ # boundary store options
33
+ boundary_offload: bool = False # move boundary activations to CPU
34
+ boundary_precision: Optional[torch.dtype] = None # e.g. torch.float16 to save memory
35
+ pin_memory: bool = False
36
+
37
+ # pipeline scheduler option
38
+ num_microbatches: int = 1
39
+
40
+ # misc
41
+ grad_clip_norm: Optional[float] = None
42
+ seed: Optional[int] = None
43
+ enable_profiler: bool = True
44
+ log_level: str = "WARNING"
45
+
46
+ def __post_init__(self):
47
+ if self.seed is not None:
48
+ torch.manual_seed(self.seed)
@@ -0,0 +1,25 @@
1
+ """
2
+ Optional, opt-in reference implementations built on top of the EAE Runtime
3
+ core. Nothing under `eae_runtime.contrib` is imported by
4
+ `eae_runtime.__init__`, and the runtime core has zero dependency on it -
5
+ this package exists purely to demonstrate and give you a starting point
6
+ for real Transformer architectures. Fork freely.
7
+ """
8
+
9
+ from .transformer_blocks import (
10
+ CausalSelfAttention,
11
+ PreNormTransformerBlock,
12
+ RMSNorm,
13
+ RotaryPositionalEmbedding,
14
+ SwiGLU,
15
+ apply_rotary,
16
+ )
17
+
18
+ __all__ = [
19
+ "CausalSelfAttention",
20
+ "PreNormTransformerBlock",
21
+ "RMSNorm",
22
+ "RotaryPositionalEmbedding",
23
+ "SwiGLU",
24
+ "apply_rotary",
25
+ ]