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,259 @@
1
+ """
2
+ eae_runtime.contrib.transformer_blocks
3
+ =======================================
4
+
5
+ Reference, current-generation (2024-2026 era) Transformer building blocks
6
+ that are known-good EAE Runtime blocks: each `nn.Module` here has a
7
+ `forward(x) -> Tensor` that is a pure function of `x` and its own
8
+ parameters/buffers - exactly the contract `ReconstructionEngine` relies on
9
+ (see `eae_runtime.blocks.EAEBlock`).
10
+
11
+ These are deliberately **not** shipped as part of the runtime core. The
12
+ runtime is architecture-agnostic by design (see README "Non-goals"); this
13
+ module exists to:
14
+
15
+ 1. Prove, concretely, that the runtime handles modern Transformer
16
+ internals: RMSNorm, rotary position embeddings (RoPE), grouped-query
17
+ attention (GQA) via `F.scaled_dot_product_attention` (which
18
+ transparently dispatches to a fused flash-attention / memory-
19
+ efficient kernel on supported hardware), and SwiGLU feed-forward
20
+ layers - the components of e.g. LLaMA / Mistral / Qwen-style models.
21
+ 2. Demonstrate the idiomatic pattern for handling information a block
22
+ needs besides `x` (a causal mask, RoPE tables) *without* breaking the
23
+ runtime's single-tensor forward contract: precompute it once in
24
+ `__init__` / `register_buffer`, never as an extra forward() argument.
25
+ 3. Give you a working, forkable starting point. Copy this file into your
26
+ own project and edit it; the runtime core never needs to change to
27
+ support your architecture.
28
+
29
+ Usage::
30
+
31
+ from eae_runtime.contrib import PreNormTransformerBlock
32
+
33
+ blocks = nn.ModuleList([
34
+ PreNormTransformerBlock(dim=512, num_heads=8, num_kv_heads=2)
35
+ for _ in range(depth)
36
+ ])
37
+ runtime = EAERuntime(blocks, optimizer, config)
38
+
39
+ Deliberately not covered here (fork and extend instead):
40
+ * Cross-attention / encoder-decoder blocks - attention over a second,
41
+ externally supplied sequence isn't expressible as a pure function of a
42
+ single `x`. Attach the memory tensor as an instance attribute the same
43
+ way RoPE tables are attached below, or override `forward` to unpack a
44
+ tensor you've concatenated/stacked upstream.
45
+ * KV-cache incremental decoding - this reference targets full-sequence
46
+ training; inference-time caching belongs in the caller's generation
47
+ loop, not the runtime.
48
+ * Mixture-of-Experts routing.
49
+ """
50
+
51
+ from __future__ import annotations
52
+
53
+ from typing import Optional, Tuple
54
+
55
+ import torch
56
+ import torch.nn as nn
57
+ import torch.nn.functional as F
58
+
59
+ from ..blocks import EAEBlock
60
+
61
+ __all__ = [
62
+ "RMSNorm",
63
+ "RotaryPositionalEmbedding",
64
+ "apply_rotary",
65
+ "CausalSelfAttention",
66
+ "SwiGLU",
67
+ "PreNormTransformerBlock",
68
+ ]
69
+
70
+
71
+ class RMSNorm(nn.Module):
72
+ """Root-Mean-Square LayerNorm (Zhang & Sennrich, 2019).
73
+
74
+ The default normalization in LLaMA / Mistral / Gemma-style models:
75
+ cheaper than LayerNorm (no mean-centering, no bias term) and matches
76
+ or beats it empirically at scale.
77
+ """
78
+
79
+ def __init__(self, dim: int, eps: float = 1e-6):
80
+ super().__init__()
81
+ self.eps = eps
82
+ self.weight = nn.Parameter(torch.ones(dim))
83
+
84
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
85
+ dtype = x.dtype
86
+ xf = x.float()
87
+ rms = torch.rsqrt(xf.pow(2).mean(dim=-1, keepdim=True) + self.eps)
88
+ return (xf * rms).to(dtype) * self.weight
89
+
90
+
91
+ class RotaryPositionalEmbedding(nn.Module):
92
+ """Rotary position embeddings (Su et al., 2021 / RoFormer) - the de
93
+ facto standard positional scheme in modern decoder-only Transformers.
94
+
95
+ Precomputes cos/sin tables as non-persistent buffers (so they move
96
+ with `.to(device)` but are never saved to a checkpoint or trained) up
97
+ to `max_seq_len`; `forward(seq_len)` slices out the prefix needed for
98
+ the current sequence.
99
+ """
100
+
101
+ cos_cached: torch.Tensor
102
+ sin_cached: torch.Tensor
103
+
104
+ def __init__(self, head_dim: int, max_seq_len: int = 4096, base: float = 10000.0):
105
+ super().__init__()
106
+ if head_dim % 2 != 0:
107
+ raise ValueError(f"RoPE requires an even head_dim, got {head_dim}")
108
+ inv_freq = 1.0 / (base ** (torch.arange(0, head_dim, 2).float() / head_dim))
109
+ t = torch.arange(max_seq_len).float()
110
+ freqs = torch.outer(t, inv_freq) # (max_seq_len, head_dim // 2)
111
+ self.register_buffer("cos_cached", freqs.cos(), persistent=False)
112
+ self.register_buffer("sin_cached", freqs.sin(), persistent=False)
113
+
114
+ def forward(self, seq_len: int) -> Tuple[torch.Tensor, torch.Tensor]:
115
+ return self.cos_cached[:seq_len], self.sin_cached[:seq_len]
116
+
117
+
118
+ def apply_rotary(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
119
+ """Apply RoPE rotation to `x` of shape (batch, heads, seq, head_dim)
120
+ given per-position `cos`/`sin` tables of shape (seq, head_dim // 2)."""
121
+ x1, x2 = x[..., 0::2], x[..., 1::2]
122
+ cos = cos[None, None, :, :].to(x.dtype)
123
+ sin = sin[None, None, :, :].to(x.dtype)
124
+ rotated = torch.stack([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)
125
+ return rotated.flatten(-2)
126
+
127
+
128
+ class CausalSelfAttention(nn.Module):
129
+ """Causal (optionally grouped-query) self-attention built on
130
+ `torch.nn.functional.scaled_dot_product_attention`, which dispatches
131
+ to a fused flash-attention / memory-efficient kernel on supported
132
+ hardware with no extra code here - the current recommended way to
133
+ write attention in PyTorch (2.x+), rather than a hand-rolled
134
+ `softmax(QK^T / sqrt(d)) @ V`.
135
+
136
+ Set `num_kv_heads < num_heads` for grouped-query attention (GQA, as in
137
+ LLaMA-2 70B / Mistral) to shrink the KV cache at inference time;
138
+ `num_kv_heads=1` is multi-query attention (MQA). Leave it as `None`
139
+ for ordinary multi-head attention.
140
+ """
141
+
142
+ def __init__(
143
+ self,
144
+ dim: int,
145
+ num_heads: int,
146
+ num_kv_heads: Optional[int] = None,
147
+ max_seq_len: int = 4096,
148
+ dropout: float = 0.0,
149
+ rope: bool = True,
150
+ ):
151
+ super().__init__()
152
+ if dim % num_heads != 0:
153
+ raise ValueError(f"dim ({dim}) must be divisible by num_heads ({num_heads})")
154
+ self.num_heads = num_heads
155
+ self.num_kv_heads = num_kv_heads or num_heads
156
+ if num_heads % self.num_kv_heads != 0:
157
+ raise ValueError("num_heads must be divisible by num_kv_heads for GQA")
158
+ self.head_dim = dim // num_heads
159
+ self.dropout = dropout
160
+
161
+ self.q_proj = nn.Linear(dim, num_heads * self.head_dim, bias=False)
162
+ self.k_proj = nn.Linear(dim, self.num_kv_heads * self.head_dim, bias=False)
163
+ self.v_proj = nn.Linear(dim, self.num_kv_heads * self.head_dim, bias=False)
164
+ self.out_proj = nn.Linear(num_heads * self.head_dim, dim, bias=False)
165
+
166
+ self.rope = RotaryPositionalEmbedding(self.head_dim, max_seq_len) if rope else None
167
+
168
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
169
+ batch, seq, _ = x.shape
170
+ q = self.q_proj(x).view(batch, seq, self.num_heads, self.head_dim).transpose(1, 2)
171
+ k = self.k_proj(x).view(batch, seq, self.num_kv_heads, self.head_dim).transpose(1, 2)
172
+ v = self.v_proj(x).view(batch, seq, self.num_kv_heads, self.head_dim).transpose(1, 2)
173
+
174
+ if self.rope is not None:
175
+ cos, sin = self.rope(seq)
176
+ q = apply_rotary(q, cos, sin)
177
+ k = apply_rotary(k, cos, sin)
178
+
179
+ if self.num_kv_heads != self.num_heads:
180
+ repeat = self.num_heads // self.num_kv_heads
181
+ k = k.repeat_interleave(repeat, dim=1)
182
+ v = v.repeat_interleave(repeat, dim=1)
183
+
184
+ out = F.scaled_dot_product_attention(
185
+ q, k, v, is_causal=True, dropout_p=self.dropout if self.training else 0.0
186
+ )
187
+ out = out.transpose(1, 2).contiguous().view(batch, seq, self.num_heads * self.head_dim)
188
+ return self.out_proj(out)
189
+
190
+
191
+ class SwiGLU(nn.Module):
192
+ """SwiGLU feed-forward (Shazeer, 2020) - the standard modern
193
+ replacement for a plain Linear -> GELU -> Linear FFN, used in
194
+ LLaMA / PaLM / Mistral. `hidden_dim` defaults to ~8/3 * dim rounded up
195
+ to a multiple of `multiple_of` for hardware-friendly matmul shapes.
196
+ """
197
+
198
+ def __init__(self, dim: int, hidden_dim: Optional[int] = None, multiple_of: int = 256):
199
+ super().__init__()
200
+ if hidden_dim is None:
201
+ hidden_dim = int(2 * (4 * dim) / 3)
202
+ hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
203
+ self.gate_proj = nn.Linear(dim, hidden_dim, bias=False)
204
+ self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
205
+ self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
206
+
207
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
208
+ return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
209
+
210
+
211
+ class PreNormTransformerBlock(EAEBlock):
212
+ """One decoder-only Transformer block in the current dominant
213
+ architecture family (LLaMA/Mistral/Qwen-style): pre-norm residual
214
+ stream, RMSNorm, causal (optionally grouped-query) attention with
215
+ RoPE, RMSNorm, SwiGLU feed-forward.
216
+
217
+ Stack `depth` of these in an `nn.ModuleList` and hand that straight to
218
+ `EAERuntime` - `BlockDecomposer` treats each block as one
219
+ reconstructable unit::
220
+
221
+ blocks = nn.ModuleList([
222
+ PreNormTransformerBlock(dim=512, num_heads=8, num_kv_heads=2)
223
+ for _ in range(depth)
224
+ ])
225
+ runtime = EAERuntime(blocks, optimizer, config)
226
+
227
+ Every sub-module here (`RMSNorm`, `CausalSelfAttention`, `SwiGLU`) is
228
+ independently reusable if your architecture only needs one piece, and
229
+ every EAE pass (`ClipPass`, `Int8QuantizationPass`, a custom
230
+ compression/synthetic-gradient pass, ...) applies to the adjoint
231
+ flowing out of this block exactly as it would for a plain `nn.Linear`.
232
+ """
233
+
234
+ def __init__(
235
+ self,
236
+ dim: int,
237
+ num_heads: int,
238
+ num_kv_heads: Optional[int] = None,
239
+ max_seq_len: int = 4096,
240
+ ffn_hidden_dim: Optional[int] = None,
241
+ dropout: float = 0.0,
242
+ norm_eps: float = 1e-6,
243
+ ):
244
+ super().__init__()
245
+ self.attn_norm = RMSNorm(dim, eps=norm_eps)
246
+ self.attn = CausalSelfAttention(
247
+ dim,
248
+ num_heads,
249
+ num_kv_heads=num_kv_heads,
250
+ max_seq_len=max_seq_len,
251
+ dropout=dropout,
252
+ )
253
+ self.ffn_norm = RMSNorm(dim, eps=norm_eps)
254
+ self.ffn = SwiGLU(dim, hidden_dim=ffn_hidden_dim)
255
+
256
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
257
+ x = x + self.attn(self.attn_norm(x))
258
+ x = x + self.ffn(self.ffn_norm(x))
259
+ return x
eae_runtime/events.py ADDED
@@ -0,0 +1,84 @@
1
+ """
2
+ Event system: everything important that happens inside the runtime emits an
3
+ Event. Researchers should never need to edit runtime code just to observe
4
+ execution - they subscribe to the EventBus instead.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Callable, Dict, List, Optional
12
+
13
+
14
+ class EventType:
15
+ FORWARD_STARTED = "ForwardStarted"
16
+ FORWARD_FINISHED = "ForwardFinished"
17
+ BLOCK_RECONSTRUCTED = "BlockReconstructed"
18
+ ADJOINT_CREATED = "AdjointCreated"
19
+ ADJOINT_MODIFIED = "AdjointModified"
20
+ MEMORY_ALLOCATED = "MemoryAllocated"
21
+ MEMORY_RELEASED = "MemoryReleased"
22
+ SCHEDULER_STEP = "SchedulerStep"
23
+ KERNEL_LAUNCH = "KernelLaunch"
24
+ COMMUNICATION_START = "CommunicationStart"
25
+ COMMUNICATION_FINISH = "CommunicationFinish"
26
+ TRAIN_STEP_STARTED = "TrainStepStarted"
27
+ TRAIN_STEP_FINISHED = "TrainStepFinished"
28
+ PASS_APPLIED = "PassApplied"
29
+
30
+
31
+ @dataclass
32
+ class Event:
33
+ type: str
34
+ payload: Dict[str, Any] = field(default_factory=dict)
35
+ timestamp: float = field(default_factory=time.time)
36
+
37
+ def __repr__(self) -> str: # pragma: no cover - cosmetic
38
+ return f"Event({self.type}, {self.payload})"
39
+
40
+
41
+ class EventBus:
42
+ """A minimal, synchronous pub/sub bus.
43
+
44
+ Subscribers are plain callables `fn(event: Event) -> None`. Subscribing
45
+ to `"*"` receives every event, regardless of type.
46
+ """
47
+
48
+ def __init__(self) -> None:
49
+ self._subscribers: Dict[str, List[Callable[[Event], None]]] = {}
50
+ self._log: List[Event] = []
51
+ self._recording = False
52
+
53
+ def subscribe(self, event_type: str, callback: Callable[[Event], None]) -> None:
54
+ self._subscribers.setdefault(event_type, []).append(callback)
55
+
56
+ def unsubscribe(self, event_type: str, callback: Callable[[Event], None]) -> None:
57
+ if event_type in self._subscribers and callback in self._subscribers[event_type]:
58
+ self._subscribers[event_type].remove(callback)
59
+
60
+ def emit(self, event_type: str, **payload: Any) -> Event:
61
+ event = Event(type=event_type, payload=payload)
62
+ if self._recording:
63
+ self._log.append(event)
64
+ for callback in self._subscribers.get(event_type, []):
65
+ callback(event)
66
+ for callback in self._subscribers.get("*", []):
67
+ callback(event)
68
+ return event
69
+
70
+ # -- recording / introspection, handy for tests -------------------- #
71
+ def start_recording(self) -> None:
72
+ self._recording = True
73
+ self._log = []
74
+
75
+ def stop_recording(self) -> List[Event]:
76
+ self._recording = False
77
+ return self._log
78
+
79
+ @property
80
+ def log(self) -> List[Event]:
81
+ return list(self._log)
82
+
83
+ def events_of_type(self, event_type: str) -> List[Event]:
84
+ return [e for e in self._log if e.type == event_type]
@@ -0,0 +1,35 @@
1
+ """
2
+ Forward Executor: detached forward, boundary extraction, boundary storage.
3
+ No autograd graph is kept alive - this is what makes the runtime
4
+ memory-cheap relative to a naive PyTorch backward.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import List, Optional
10
+
11
+ import torch
12
+ import torch.nn as nn
13
+
14
+ from .boundary_store import BoundaryStore
15
+ from .events import EventBus, EventType
16
+
17
+
18
+ class ForwardExecutor:
19
+ def __init__(self, event_bus: Optional[EventBus] = None, compute_dtype: Optional[torch.dtype] = None):
20
+ self.event_bus = event_bus or EventBus()
21
+ self.compute_dtype = compute_dtype
22
+
23
+ @torch.no_grad()
24
+ def run(self, blocks: List[nn.Module], x0: torch.Tensor, boundary_store: BoundaryStore) -> torch.Tensor:
25
+ self.event_bus.emit(EventType.FORWARD_STARTED, num_blocks=len(blocks))
26
+
27
+ x = x0
28
+ boundary_store.put(0, x)
29
+ for i, block in enumerate(blocks):
30
+ compute_x = x.to(self.compute_dtype) if self.compute_dtype is not None else x
31
+ x = block(compute_x)
32
+ boundary_store.put(i + 1, x)
33
+
34
+ self.event_bus.emit(EventType.FORWARD_FINISHED, num_boundaries=len(boundary_store))
35
+ return x
@@ -0,0 +1,41 @@
1
+ """
2
+ Structured logging: every runtime event should be observable. This wires
3
+ the EventBus to Python's logging module using a JSON-ish structured
4
+ formatter, so events can be piped to any log aggregator.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ from typing import Optional
12
+
13
+ from .events import Event, EventBus
14
+
15
+ _LOGGER_NAME = "eae_runtime"
16
+
17
+
18
+ def get_logger(level: str = "WARNING") -> logging.Logger:
19
+ logger = logging.getLogger(_LOGGER_NAME)
20
+ if not logger.handlers:
21
+ handler = logging.StreamHandler()
22
+ handler.setFormatter(logging.Formatter("%(message)s"))
23
+ logger.addHandler(handler)
24
+ logger.setLevel(getattr(logging, level.upper(), logging.WARNING))
25
+ return logger
26
+
27
+
28
+ def attach_structured_logging(event_bus: EventBus, level: str = "WARNING", logger: Optional[logging.Logger] = None) -> None:
29
+ """Subscribe a structured JSON logger to every event on `event_bus`.
30
+
31
+ `level` sets the logger's threshold (e.g. "WARNING" hides events by
32
+ default). Individual event records are always emitted at DEBUG
33
+ severity, so setting `level="DEBUG"` is what makes them visible.
34
+ """
35
+ logger = logger or get_logger(level)
36
+
37
+ def _log(event: Event) -> None:
38
+ record = {"event": event.type, "timestamp": event.timestamp, **event.payload}
39
+ logger.debug(json.dumps(record, default=str))
40
+
41
+ event_bus.subscribe("*", _log)
eae_runtime/memory.py ADDED
@@ -0,0 +1,175 @@
1
+ """
2
+ Memory Manager: owns temporary tensors, scratch buffers, reconstruction
3
+ workspace. Provides request()/release()/reuse() and exposes runtime
4
+ statistics. The allocator should minimize allocations, fragmentation and
5
+ synchronization.
6
+
7
+ Two policies are provided out of the box:
8
+ * PoolMemoryPolicy - buckets free tensors by (shape, dtype, device) and
9
+ reuses them, avoiding repeated cudaMalloc/alloc churn.
10
+ * NullMemoryPolicy - always allocates fresh / frees immediately; useful
11
+ as a correctness baseline and for leak tests.
12
+
13
+ Users can implement their own MemoryPolicy (Memory Policy API) without
14
+ touching reconstruction logic.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import threading
20
+ from typing import Dict, Optional, Tuple
21
+
22
+ import torch
23
+
24
+ from .events import EventBus, EventType
25
+
26
+ PoolKey = Tuple[Tuple[int, ...], torch.dtype, str]
27
+
28
+
29
+ class MemoryPolicy:
30
+ """Base class for pluggable memory policies."""
31
+
32
+ def allocate(self, shape, dtype, device) -> torch.Tensor:
33
+ raise NotImplementedError
34
+
35
+ def free(self, tensor: torch.Tensor) -> None:
36
+ raise NotImplementedError
37
+
38
+ def stats(self) -> Dict[str, int]:
39
+ raise NotImplementedError
40
+
41
+ def reset(self) -> None:
42
+ raise NotImplementedError
43
+
44
+
45
+ class NullMemoryPolicy(MemoryPolicy):
46
+ """No pooling: every request is a fresh allocation, every release is a
47
+ real deallocation (subject to Python/CUDA garbage collection)."""
48
+
49
+ def __init__(self):
50
+ self._active = 0
51
+ self._peak = 0
52
+ self._allocations = 0
53
+
54
+ def allocate(self, shape, dtype, device) -> torch.Tensor:
55
+ t = torch.empty(shape, dtype=dtype, device=device)
56
+ self._active += 1
57
+ self._allocations += 1
58
+ self._peak = max(self._peak, self._active)
59
+ return t
60
+
61
+ def free(self, tensor: torch.Tensor) -> None:
62
+ self._active = max(0, self._active - 1)
63
+
64
+ def stats(self) -> Dict[str, int]:
65
+ return {"active": self._active, "peak": self._peak, "allocations": self._allocations, "pooled": 0}
66
+
67
+ def reset(self) -> None:
68
+ self._active = 0
69
+ self._peak = 0
70
+ self._allocations = 0
71
+
72
+
73
+ class PoolMemoryPolicy(MemoryPolicy):
74
+ """Reuses freed tensors of matching (shape, dtype, device) instead of
75
+ reallocating. Thread-safe (relevant for the AsyncScheduler)."""
76
+
77
+ def __init__(self, max_pool_per_key: int = 8):
78
+ self._pool: Dict[PoolKey, list] = {}
79
+ self._lock = threading.Lock()
80
+ self._active = 0
81
+ self._peak = 0
82
+ self._allocations = 0
83
+ self._reuses = 0
84
+ self._max_pool_per_key = max_pool_per_key
85
+
86
+ @staticmethod
87
+ def _key(shape, dtype, device) -> PoolKey:
88
+ return (tuple(shape), dtype, str(device))
89
+
90
+ def allocate(self, shape, dtype, device) -> torch.Tensor:
91
+ key = self._key(shape, dtype, device)
92
+ with self._lock:
93
+ bucket = self._pool.get(key)
94
+ if bucket:
95
+ t = bucket.pop()
96
+ self._reuses += 1
97
+ self._active += 1
98
+ self._peak = max(self._peak, self._active)
99
+ return t
100
+ self._allocations += 1
101
+ self._active += 1
102
+ self._peak = max(self._peak, self._active)
103
+ return torch.empty(shape, dtype=dtype, device=device)
104
+
105
+ def free(self, tensor: torch.Tensor) -> None:
106
+ key = self._key(tensor.shape, tensor.dtype, tensor.device)
107
+ with self._lock:
108
+ self._active = max(0, self._active - 1)
109
+ bucket = self._pool.setdefault(key, [])
110
+ if len(bucket) < self._max_pool_per_key:
111
+ bucket.append(tensor.detach())
112
+
113
+ def stats(self) -> Dict[str, int]:
114
+ pooled = sum(len(v) for v in self._pool.values())
115
+ return {
116
+ "active": self._active,
117
+ "peak": self._peak,
118
+ "allocations": self._allocations,
119
+ "reuses": self._reuses,
120
+ "pooled": pooled,
121
+ }
122
+
123
+ def reset(self) -> None:
124
+ with self._lock:
125
+ self._pool.clear()
126
+ self._active = 0
127
+ self._peak = 0
128
+ self._allocations = 0
129
+ self._reuses = 0
130
+
131
+
132
+ _POLICIES = {
133
+ "pool": PoolMemoryPolicy,
134
+ "none": NullMemoryPolicy,
135
+ }
136
+
137
+
138
+ class MemoryManager:
139
+ """Facade used by the rest of the runtime. Wraps a MemoryPolicy and adds
140
+ event emission + a scratch-buffer convenience API."""
141
+
142
+ def __init__(self, policy="pool", event_bus: Optional[EventBus] = None):
143
+ if isinstance(policy, str):
144
+ if policy not in _POLICIES:
145
+ raise ValueError(f"Unknown memory policy '{policy}', choices: {list(_POLICIES)}")
146
+ policy = _POLICIES[policy]()
147
+ elif isinstance(policy, type) and issubclass(policy, MemoryPolicy):
148
+ policy = policy()
149
+ self.policy: MemoryPolicy = policy
150
+ self.event_bus = event_bus or EventBus()
151
+
152
+ def request(self, shape, dtype=torch.float32, device="cpu") -> torch.Tensor:
153
+ t = self.policy.allocate(shape, dtype, device)
154
+ self.event_bus.emit(EventType.MEMORY_ALLOCATED, shape=tuple(shape), dtype=str(dtype), device=str(device))
155
+ return t
156
+
157
+ def release(self, tensor: torch.Tensor) -> None:
158
+ if tensor is None:
159
+ return
160
+ shape = tuple(tensor.shape)
161
+ self.policy.free(tensor)
162
+ self.event_bus.emit(EventType.MEMORY_RELEASED, shape=shape)
163
+
164
+ def reuse(self, tensor: torch.Tensor) -> torch.Tensor:
165
+ """Zero out and hand back a tensor for reuse without a full
166
+ free+allocate round trip."""
167
+ with torch.no_grad():
168
+ tensor.zero_()
169
+ return tensor
170
+
171
+ def stats(self) -> Dict[str, int]:
172
+ return self.policy.stats()
173
+
174
+ def reset(self) -> None:
175
+ self.policy.reset()
@@ -0,0 +1,18 @@
1
+ from .base import EAEPass
2
+ from .clip import ClipPass
3
+ from .quantize import FP16Pass, FP8Pass, Int8QuantizationPass
4
+ from .synthetic_gradient import SyntheticGradientPass
5
+ from .regularization import RegularizationPass, GaussianNoisePass
6
+ from .logging_pass import LoggingPass
7
+
8
+ __all__ = [
9
+ "EAEPass",
10
+ "ClipPass",
11
+ "FP16Pass",
12
+ "FP8Pass",
13
+ "Int8QuantizationPass",
14
+ "SyntheticGradientPass",
15
+ "RegularizationPass",
16
+ "GaussianNoisePass",
17
+ "LoggingPass",
18
+ ]
@@ -0,0 +1,31 @@
1
+ """
2
+ Pass API: every adjoint passes through Pass1 -> Pass2 -> ... -> Passn before
3
+ reaching the next block. Passes are plugins; the runtime never knows what
4
+ they do.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Optional
10
+
11
+ from ..adjoint import AdjointState
12
+
13
+
14
+ class EAEPass:
15
+ """Base class for a single stage in the Adjoint Pipeline.
16
+
17
+ Subclasses implement `process(adjoint, context) -> AdjointState`.
18
+ `context` is a plain dict with useful ambient info (current block name,
19
+ layer_id, training step, event_bus, etc.) that a pass may read but
20
+ should not mutate destructively.
21
+ """
22
+
23
+ name: str = "EAEPass"
24
+
25
+ def process(self, adjoint: AdjointState, context: Optional[dict] = None) -> AdjointState:
26
+ raise NotImplementedError
27
+
28
+ def __call__(self, adjoint: AdjointState, context: Optional[dict] = None) -> AdjointState:
29
+ result = self.process(adjoint, context or {})
30
+ result.record(self.name)
31
+ return result
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+
5
+ from ..adjoint import AdjointState
6
+ from .base import EAEPass
7
+
8
+
9
+ class ClipPass(EAEPass):
10
+ """Clips the adjoint tensor's norm to `max_norm`, mirroring
11
+ torch.nn.utils.clip_grad_norm_ but operating on a single adjoint in the
12
+ pipeline rather than a full parameter list."""
13
+
14
+ name = "ClipPass"
15
+
16
+ def __init__(self, max_norm: float = 1.0):
17
+ self.max_norm = max_norm
18
+
19
+ def process(self, adjoint: AdjointState, context=None) -> AdjointState:
20
+ new = adjoint.clone()
21
+ norm = torch.linalg.vector_norm(new.tensor.float())
22
+ if norm > self.max_norm:
23
+ scale = self.max_norm / (norm + 1e-6)
24
+ new.tensor = new.tensor * scale.to(new.tensor.dtype)
25
+ return new