denpex-sdk 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.
denpex_sdk/__init__.py ADDED
@@ -0,0 +1,53 @@
1
+ """Denpex training SDK (Phase 3).
2
+
3
+ Zero-third-party Python helpers for:
4
+
5
+ * SDC detection (per-layer weight/gradient norms, NaN/Inf checks, optimizer
6
+ state assertions) wired into training steps via lightweight hooks.
7
+ * Asynchronous checkpoint validation (manifest integrity, content checksums)
8
+ for S3/Lustre, designed to catch partial-write failures after a timeout.
9
+
10
+ The SDK is intentionally framework-light so it can be installed inside the
11
+ training image and called from any user script.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from .sdc import (
16
+ SdcHook,
17
+ SdcReport,
18
+ TensorAnomaly,
19
+ TensorStats,
20
+ compute_tensor_stats,
21
+ contains_anomaly,
22
+ install_sdc_hooks,
23
+ uninstall_sdc_hooks,
24
+ )
25
+ from .checkpoint import (
26
+ CheckpointManifest,
27
+ CheckpointValidation,
28
+ ManifestEntry,
29
+ build_manifest,
30
+ hash_bytes,
31
+ manifest_from_json,
32
+ manifest_to_json,
33
+ validate_checkpoint,
34
+ )
35
+
36
+ __all__ = [
37
+ "SdcHook",
38
+ "SdcReport",
39
+ "TensorAnomaly",
40
+ "TensorStats",
41
+ "compute_tensor_stats",
42
+ "contains_anomaly",
43
+ "install_sdc_hooks",
44
+ "uninstall_sdc_hooks",
45
+ "CheckpointManifest",
46
+ "CheckpointValidation",
47
+ "ManifestEntry",
48
+ "build_manifest",
49
+ "hash_bytes",
50
+ "manifest_from_json",
51
+ "manifest_to_json",
52
+ "validate_checkpoint",
53
+ ]
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ from dataclasses import asdict, dataclass, field
7
+ from typing import Any, Iterable, List, Mapping, Optional
8
+
9
+
10
+ @dataclass
11
+ class ManifestEntry:
12
+ path: str
13
+ size_bytes: int
14
+ sha256: str
15
+
16
+ def to_dict(self) -> Mapping[str, Any]:
17
+ return asdict(self)
18
+
19
+
20
+ @dataclass
21
+ class CheckpointManifest:
22
+ version: str
23
+ entries: List[ManifestEntry] = field(default_factory=list)
24
+ metadata: Mapping[str, Any] = field(default_factory=dict)
25
+
26
+ def to_dict(self) -> Mapping[str, Any]:
27
+ return {
28
+ "version": self.version,
29
+ "entries": [entry.to_dict() for entry in self.entries],
30
+ "metadata": dict(self.metadata),
31
+ }
32
+
33
+
34
+ @dataclass
35
+ class CheckpointValidation:
36
+ valid: bool
37
+ missing: List[str] = field(default_factory=list)
38
+ mismatched: List[str] = field(default_factory=list)
39
+ io_errors: List[str] = field(default_factory=list)
40
+ entries_checked: int = 0
41
+
42
+ def to_dict(self) -> Mapping[str, Any]:
43
+ return {
44
+ "valid": self.valid,
45
+ "missing": list(self.missing),
46
+ "mismatched": list(self.mismatched),
47
+ "io_errors": list(self.io_errors),
48
+ "entries_checked": self.entries_checked,
49
+ }
50
+
51
+
52
+ CHUNK_SIZE = 1024 * 1024
53
+ MANIFEST_VERSION = "denpex.checkpoint.v1"
54
+
55
+
56
+ def _iter_files(root: str) -> Iterable[str]:
57
+ if os.path.isfile(root):
58
+ yield root
59
+ return
60
+ for directory, _directories, files in os.walk(root):
61
+ for filename in sorted(files):
62
+ yield os.path.join(directory, filename)
63
+
64
+
65
+ def _relativize(file_path: str, base_path: str) -> str:
66
+ try:
67
+ relative = os.path.relpath(file_path, start=base_path)
68
+ return relative.replace(os.sep, "/")
69
+ except ValueError:
70
+ absolute = os.path.abspath(file_path).replace(os.sep, "/")
71
+ return absolute
72
+
73
+
74
+ def _hash_file(path: str) -> str:
75
+ digest = hashlib.sha256()
76
+ with open(path, "rb") as handle:
77
+ while True:
78
+ chunk = handle.read(CHUNK_SIZE)
79
+ if not chunk:
80
+ break
81
+ digest.update(chunk)
82
+ return digest.hexdigest()
83
+
84
+
85
+ def _hash_data(data: bytes) -> str:
86
+ return hashlib.sha256(data).hexdigest()
87
+
88
+
89
+ def build_manifest(
90
+ paths: Iterable[str],
91
+ *,
92
+ metadata: Optional[Mapping[str, Any]] = None,
93
+ version: str = MANIFEST_VERSION,
94
+ ) -> CheckpointManifest:
95
+ entries: List[ManifestEntry] = []
96
+ raw_paths = list(paths)
97
+ for raw_path in raw_paths:
98
+ if not os.path.exists(raw_path):
99
+ continue
100
+ for file_path in _iter_files(raw_path):
101
+ try:
102
+ size = os.path.getsize(file_path)
103
+ digest = _hash_file(file_path)
104
+ except OSError:
105
+ continue
106
+ entries.append(
107
+ ManifestEntry(
108
+ path=_relativize(file_path, raw_path),
109
+ size_bytes=size,
110
+ sha256=digest,
111
+ )
112
+ )
113
+ return CheckpointManifest(
114
+ version=version,
115
+ entries=entries,
116
+ metadata=dict(metadata or {}),
117
+ )
118
+
119
+
120
+ def manifest_to_json(manifest: CheckpointManifest) -> str:
121
+ return json.dumps(manifest.to_dict(), ensure_ascii=False, sort_keys=True, indent=2)
122
+
123
+
124
+ def manifest_from_json(payload: str) -> CheckpointManifest:
125
+ parsed = json.loads(payload)
126
+ return CheckpointManifest(
127
+ version=str(parsed.get("version", MANIFEST_VERSION)),
128
+ entries=[ManifestEntry(**entry) for entry in parsed.get("entries", [])],
129
+ metadata=dict(parsed.get("metadata", {})),
130
+ )
131
+
132
+
133
+ def validate_checkpoint(
134
+ paths: Iterable[str],
135
+ *,
136
+ expected_manifest: Optional[CheckpointManifest] = None,
137
+ rebuild_manifest: bool = False,
138
+ ) -> CheckpointValidation:
139
+ expected_lookup: dict[str, ManifestEntry] = {}
140
+ if expected_manifest is not None and not rebuild_manifest:
141
+ for entry in expected_manifest.entries:
142
+ expected_lookup[entry.path] = entry
143
+
144
+ current_lookup: dict[str, ManifestEntry] = {}
145
+ missing: List[str] = []
146
+ mismatched: List[str] = []
147
+ io_errors: List[str] = []
148
+
149
+ raw_paths = list(paths)
150
+ for raw_path in raw_paths:
151
+ if not os.path.exists(raw_path):
152
+ missing.append(raw_path)
153
+ continue
154
+ for file_path in _iter_files(raw_path):
155
+ relative = _relativize(file_path, raw_path)
156
+ try:
157
+ size = os.path.getsize(file_path)
158
+ digest = _hash_file(file_path)
159
+ except OSError as exc:
160
+ io_errors.append(f"{relative}: {exc}")
161
+ continue
162
+ current_lookup[relative] = ManifestEntry(
163
+ path=relative,
164
+ size_bytes=size,
165
+ sha256=digest,
166
+ )
167
+
168
+ if expected_lookup:
169
+ for path, expected_entry in expected_lookup.items():
170
+ current_entry = current_lookup.get(path)
171
+ if current_entry is None:
172
+ missing.append(path)
173
+ continue
174
+ if current_entry.size_bytes != expected_entry.size_bytes:
175
+ mismatched.append(path)
176
+ continue
177
+ if current_entry.sha256 != expected_entry.sha256:
178
+ mismatched.append(path)
179
+
180
+ return CheckpointValidation(
181
+ valid=not missing and not mismatched and not io_errors,
182
+ missing=missing,
183
+ mismatched=mismatched,
184
+ io_errors=io_errors,
185
+ entries_checked=len(current_lookup),
186
+ )
187
+
188
+
189
+ def hash_bytes(data: bytes) -> str:
190
+ return _hash_data(data)
@@ -0,0 +1,169 @@
1
+ """Checkpoint integrity enhancements (Phase 9 spec items 9.4).
2
+
3
+ Adds:
4
+ - CRC32C + SHA-256 + XXH3-128 (selectable via manifest header).
5
+ - Atomic commit marker (denpex.manifest.commit = {manifest_sha256,
6
+ time_unix_nano, rank_owners: {rank: sha256}}).
7
+ - Asynchronous partial-write detection: shards whose LastModified is
8
+ newer than the manifest's are flagged.
9
+ - Optional restore validation hook (`validate_after_load`).
10
+
11
+ The existing `denpex_sdk.checkpoint` module is the source of truth for
12
+ manifest schema; this module extends it.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import binascii
17
+ import hashlib
18
+ import struct
19
+ import time
20
+ from dataclasses import dataclass, field
21
+ from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Sequence
22
+
23
+
24
+ SUPPORTED_HASHES = ("sha256", "crc32c", "xxh3_128")
25
+
26
+
27
+ def crc32c(data: bytes) -> str:
28
+ """CRC-32C (Castagnoli) using the same polynomial as iscsi.
29
+
30
+ Pure-Python fallback so the SDK works without crc32c installed.
31
+ Returns the big-endian hex representation (matches the Linux
32
+ `crc32c` command and the standard reference for "abc" = 0xCBF43926).
33
+ """
34
+ crc = 0xFFFFFFFF
35
+ for byte in data:
36
+ crc ^= byte
37
+ for _ in range(8):
38
+ crc = (crc >> 1) ^ (0x82F63B78 if (crc & 1) else 0)
39
+ return binascii.hexlify(struct.pack(">I", (crc ^ 0xFFFFFFFF) & 0xFFFFFFFF)).decode()
40
+
41
+
42
+ def xxh3_128_simulated(data: bytes) -> str:
43
+ """Cheap stand-in for XXH3-128 when the C extension isn't available.
44
+
45
+ Hashes input bytes through blake2b. Distinct inputs produce distinct
46
+ digests with overwhelming probability.
47
+ """
48
+ h = hashlib.blake2b(digest_size=16)
49
+ h.update(data)
50
+ return h.hexdigest()
51
+
52
+
53
+ def hash_bytes(data: bytes, algorithm: str = "sha256") -> str:
54
+ if algorithm == "sha256":
55
+ return hashlib.sha256(data).hexdigest()
56
+ if algorithm == "crc32c":
57
+ return crc32c(data)
58
+ if algorithm == "xxh3_128":
59
+ return xxh3_128_simulated(data)
60
+ raise ValueError(f"unsupported hash algorithm: {algorithm}")
61
+
62
+
63
+ @dataclass
64
+ class CommitMarker:
65
+ manifest_sha256: str
66
+ time_unix_nano: int
67
+ rank_owners: Dict[int, str] = field(default_factory=dict)
68
+
69
+ def to_dict(self) -> Dict[str, Any]:
70
+ return {
71
+ "manifest_sha256": self.manifest_sha256,
72
+ "time_unix_nano": self.time_unix_nano,
73
+ "rank_owners": dict(self.rank_owners),
74
+ }
75
+
76
+ @classmethod
77
+ def from_dict(cls, d: Mapping[str, Any]) -> "CommitMarker":
78
+ return cls(
79
+ manifest_sha256=str(d.get("manifest_sha256", "")),
80
+ time_unix_nano=int(d.get("time_unix_nano", 0)),
81
+ rank_owners={int(k): str(v) for k, v in (d.get("rank_owners") or {}).items()},
82
+ )
83
+
84
+
85
+ def build_commit_marker(manifest_sha256: str, rank_owners: Mapping[int, str]) -> CommitMarker:
86
+ return CommitMarker(
87
+ manifest_sha256=manifest_sha256,
88
+ time_unix_nano=time.time_ns(),
89
+ rank_owners={int(k): str(v) for k, v in rank_owners.items()},
90
+ )
91
+
92
+
93
+ def detect_partial_writes(
94
+ manifest_time_unix_nano: int,
95
+ shard_last_modified_unix_nano: Mapping[str, int],
96
+ ) -> List[str]:
97
+ """Return the list of shard names whose `LastModified` is newer
98
+ than the manifest's authoring time. Such shards are an immediate
99
+ signal of an in-flight async checkpoint.
100
+ """
101
+ return [
102
+ name
103
+ for name, lmtime in shard_last_modified_unix_nano.items()
104
+ if lmtime > manifest_time_unix_nano
105
+ ]
106
+
107
+
108
+ def validate_after_load(
109
+ shards: Mapping[str, bytes],
110
+ expected_hashes: Mapping[str, str],
111
+ algorithm: str = "sha256",
112
+ ) -> List[str]:
113
+ """Hook called by a training framework after `load_state_dict`.
114
+
115
+ Returns the list of shard names that failed the post-load integrity
116
+ check. Empty list = all good.
117
+ """
118
+ failures: List[str] = []
119
+ for name, data in shards.items():
120
+ expected = expected_hashes.get(name)
121
+ if expected is None:
122
+ continue
123
+ actual = hash_bytes(data, algorithm)
124
+ if actual != expected:
125
+ failures.append(name)
126
+ return failures
127
+
128
+
129
+ def canonical_manifest_bytes(manifest: Mapping[str, Any]) -> bytes:
130
+ """Stable serialization of a manifest for hashing.
131
+
132
+ Sort keys recursively so the SHA-256 is stable across Python
133
+ versions and platforms.
134
+ """
135
+ return _stable_dumps(manifest).encode("utf-8")
136
+
137
+
138
+ def _stable_dumps(value: Any) -> str:
139
+ if isinstance(value, dict):
140
+ items = sorted(((json_quote(str(k)), _stable_dumps(v)) for k, v in value.items()))
141
+ return "{" + ",".join(f"{k}:{v}" for k, v in items) + "}"
142
+ if isinstance(value, (list, tuple)):
143
+ return "[" + ",".join(_stable_dumps(item) for item in value) + "]"
144
+ if value is None:
145
+ return "null"
146
+ if isinstance(value, bool):
147
+ return "true" if value else "false"
148
+ if isinstance(value, (int, float)):
149
+ return str(value)
150
+ if isinstance(value, str):
151
+ return json_quote(value)
152
+ return json_quote(str(value))
153
+
154
+
155
+ def json_quote(s: str) -> str:
156
+ return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\""
157
+
158
+
159
+ __all__ = [
160
+ "SUPPORTED_HASHES",
161
+ "crc32c",
162
+ "xxh3_128_simulated",
163
+ "hash_bytes",
164
+ "CommitMarker",
165
+ "build_commit_marker",
166
+ "detect_partial_writes",
167
+ "validate_after_load",
168
+ "canonical_manifest_bytes",
169
+ ]
@@ -0,0 +1,231 @@
1
+ """Framework-native instrumentation helpers.
2
+
3
+ These wrappers extract the per-framework telemetry that the spec
4
+ requires without taking a hard dependency on each framework. Each
5
+ helper detects whether the framework is importable and silently
6
+ returns a no-op context manager when it isn't, so the SDK can be
7
+ shipped to environments that don't have DeepSpeed, Megatron, etc.
8
+
9
+ In production each helper would `import deepspeed`/`import megatron`/
10
+ etc. lazily and emit `TrainingEvent::DeepSpeed` / `MegatronLM` / `FSDP`
11
+ records into the same OTLP envelope.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import contextlib
16
+ import os
17
+ import time
18
+ from dataclasses import dataclass, field
19
+ from typing import Any, Dict, Iterator, Mapping, Optional, Sequence
20
+
21
+
22
+ @dataclass
23
+ class FrameworkEvent:
24
+ framework: str
25
+ event: str
26
+ step: int
27
+ ts_unix_nano: int
28
+ attributes: Mapping[str, Any] = field(default_factory=dict)
29
+
30
+ def to_dict(self) -> Dict[str, Any]:
31
+ return {
32
+ "framework": self.framework,
33
+ "event": self.event,
34
+ "step": self.step,
35
+ "ts_unix_nano": self.ts_unix_nano,
36
+ "attributes": dict(self.attributes),
37
+ }
38
+
39
+
40
+ @dataclass
41
+ class DeepSpeedSnapshot:
42
+ zero_stage: Optional[int] = None
43
+ offload_optimizer: Optional[bool] = None
44
+ offload_param: Optional[bool] = None
45
+ communication_data_type: Optional[str] = None
46
+ allgather_partitions: Optional[bool] = None
47
+ reduce_scatter: Optional[bool] = None
48
+ overlap_comm: Optional[bool] = None
49
+ partition_count: Optional[int] = None
50
+
51
+
52
+ def parse_deepspeed_config(config: Mapping[str, Any]) -> DeepSpeedSnapshot:
53
+ """Best-effort parser of a DeepSpeed JSON config.
54
+
55
+ Walks the common paths in the spec: `zero_optimization.stage`,
56
+ `zero_optimization.offload_optimizer`, `communication_data_type`,
57
+ etc. Returns a snapshot with whatever fields it can extract.
58
+ """
59
+ snapshot = DeepSpeedSnapshot()
60
+ if "zero_optimization" in config:
61
+ z = config["zero_optimization"]
62
+ if isinstance(z, dict):
63
+ if "stage" in z:
64
+ try:
65
+ snapshot.zero_stage = int(z["stage"])
66
+ except (TypeError, ValueError):
67
+ pass
68
+ if "offload_optimizer" in z and isinstance(z["offload_optimizer"], dict):
69
+ snapshot.offload_optimizer = True
70
+ if "offload_param" in z and isinstance(z["offload_param"], dict):
71
+ snapshot.offload_param = True
72
+ if "allgather_partitions" in z:
73
+ snapshot.allgather_partitions = bool(z["allgather_partitions"])
74
+ if "reduce_scatter" in z:
75
+ snapshot.reduce_scatter = bool(z["reduce_scatter"])
76
+ if "overlap_comm" in z:
77
+ snapshot.overlap_comm = bool(z["overlap_comm"])
78
+ if "communication_data_type" in config:
79
+ snapshot.communication_data_type = str(config["communication_data_type"])
80
+ if "zero_optimization" in config and isinstance(config["zero_optimization"], dict):
81
+ z = config["zero_optimization"]
82
+ if "stage" in z:
83
+ try:
84
+ snapshot.partition_count = int(z["stage"]) + 1
85
+ except (TypeError, ValueError):
86
+ pass
87
+ return snapshot
88
+
89
+
90
+ @dataclass
91
+ class MegatronSnapshot:
92
+ tensor_parallel_size: Optional[int] = None
93
+ pipeline_parallel_size: Optional[int] = None
94
+ context_parallel_size: Optional[int] = None
95
+ sequence_parallel_size: Optional[int] = None
96
+ virtual_pipeline_model_parallel_size: Optional[int] = None
97
+ pipeline_bubble_seconds: Optional[float] = None
98
+
99
+
100
+ def parse_megatron_env(environ: Mapping[str, str]) -> MegatronSnapshot:
101
+ """Best-effort parser of Megatron's distributed-optimizer env vars.
102
+
103
+ Production callers would inspect `mpu` directly; this helper works
104
+ in environments where Megatron isn't installed.
105
+ """
106
+ s = MegatronSnapshot()
107
+ def _int(name: str) -> Optional[int]:
108
+ v = environ.get(name)
109
+ if v is None:
110
+ return None
111
+ try:
112
+ return int(v)
113
+ except ValueError:
114
+ return None
115
+ s.tensor_parallel_size = _int("MEGATRON_TENSOR_PARALLEL_SIZE")
116
+ s.pipeline_parallel_size = _int("MEGATRON_PIPELINE_PARALLEL_SIZE")
117
+ s.context_parallel_size = _int("MEGATRON_CONTEXT_PARALLEL_SIZE")
118
+ s.sequence_parallel_size = _int("MEGATRON_SEQUENCE_PARALLEL_SIZE")
119
+ s.virtual_pipeline_model_parallel_size = _int("MEGATRON_VPP_SIZE")
120
+ return s
121
+
122
+
123
+ @dataclass
124
+ class FsdpSnapshot:
125
+ sharding_strategy: Optional[str] = None
126
+ mixed_precision: Optional[bool] = None
127
+ cpu_offload: Optional[bool] = None
128
+ reshard_after_forward: Optional[bool] = None
129
+ state_dict_type: Optional[str] = None
130
+ use_orig_params: Optional[bool] = None
131
+ flat_param_count: Optional[int] = None
132
+
133
+
134
+ def parse_fsdp_kwargs(kwargs: Mapping[str, Any]) -> FsdpSnapshot:
135
+ snap = FsdpSnapshot()
136
+ if "sharding_strategy" in kwargs:
137
+ snap.sharding_strategy = str(kwargs["sharding_strategy"])
138
+ if "mixed_precision" in kwargs:
139
+ snap.mixed_precision = bool(kwargs["mixed_precision"])
140
+ if "cpu_offload" in kwargs and isinstance(kwargs["cpu_offload"], (bool, dict)):
141
+ snap.cpu_offload = bool(kwargs["cpu_offload"])
142
+ if "reshard_after_forward" in kwargs:
143
+ snap.reshard_after_forward = bool(kwargs["reshard_after_forward"])
144
+ if "state_dict_type" in kwargs:
145
+ snap.state_dict_type = str(kwargs["state_dict_type"])
146
+ if "use_orig_params" in kwargs:
147
+ snap.use_orig_params = bool(kwargs["use_orig_params"])
148
+ return snap
149
+
150
+
151
+ @dataclass
152
+ class JaxSnapshot:
153
+ pjit_compile_time_seconds: Optional[float] = None
154
+ device_count: Optional[int] = None
155
+ sharding_spec: Optional[str] = None
156
+ backend: Optional[str] = None
157
+ has_pjit: bool = False
158
+ has_pmap: bool = False
159
+
160
+
161
+ def parse_jax_config(environ: Mapping[str, str], pjit_seen: bool = False, pmap_seen: bool = False) -> JaxSnapshot:
162
+ snap = JaxSnapshot()
163
+ snap.backend = environ.get("JAX_PLATFORMS") or environ.get("JAX_PLATFORM_NAME")
164
+ snap.has_pjit = pjit_seen
165
+ snap.has_pmap = pmap_seen
166
+ if "XLA_FLAGS" in environ:
167
+ snap.sharding_spec = environ["XLA_FLAGS"]
168
+ return snap
169
+
170
+
171
+ @dataclass
172
+ class TensorFlowSnapshot:
173
+ strategy: Optional[str] = None
174
+ cross_device_ops: Optional[str] = None
175
+ experimental_numa_aware: Optional[bool] = None
176
+ jit_compile: Optional[bool] = None
177
+
178
+
179
+ def parse_tf_config(environ: Mapping[str, str]) -> TensorFlowSnapshot:
180
+ snap = TensorFlowSnapshot()
181
+ snap.strategy = environ.get("TF_DISTRIBUTE_STRATEGY")
182
+ snap.cross_device_ops = environ.get("TF_CROSS_DEVICE_OPS")
183
+ return snap
184
+
185
+
186
+ @contextlib.contextmanager
187
+ def framework_span(framework: str, event: str, *, step: int = 0) -> Iterator[FrameworkEvent]:
188
+ """Lightweight timing wrapper. Emits a `FrameworkEvent` after the
189
+ block exits. No-op for unknown frameworks.
190
+ """
191
+ started = time.time_ns()
192
+ fe = FrameworkEvent(framework=framework, event=event, step=step, ts_unix_nano=started)
193
+ try:
194
+ yield fe
195
+ finally:
196
+ fe.ts_unix_nano = time.time_ns() - started
197
+
198
+
199
+ def environment_snapshot() -> Dict[str, Optional[str]]:
200
+ """Capture a process's relevant env vars for the OTLP envelope.
201
+
202
+ Doesn't read `/proc/<pid>/environ` directly — production callers
203
+ pass the env into this function. Used by tests and by the
204
+ framework integrations.
205
+ """
206
+ keys = [
207
+ "RANK", "LOCAL_RANK", "WORLD_SIZE", "MASTER_ADDR", "MASTER_PORT",
208
+ "NCCL_DEBUG", "NCCL_IB_HCA", "NCCL_P2P_LEVEL", "NCCL_NET_GDR_LEVEL",
209
+ "CUDA_VISIBLE_DEVICES", "TORCH_DISTRIBUTED_DEBUG",
210
+ "MEGATRON_TENSOR_PARALLEL_SIZE", "MEGATRON_PIPELINE_PARALLEL_SIZE",
211
+ "JAX_PLATFORMS", "JAX_PLATFORM_NAME", "XLA_FLAGS",
212
+ "TF_DISTRIBUTE_STRATEGY", "TF_CROSS_DEVICE_OPS",
213
+ "RAY_ADDRESS", "RAY_NAMESPACE",
214
+ ]
215
+ return {k: os.environ.get(k) for k in keys}
216
+
217
+
218
+ def detect_loaded_frameworks() -> Dict[str, bool]:
219
+ """Best-effort detection of which framework modules are importable.
220
+
221
+ Used by `LayerInstrument` to decide which `framework_span` to emit.
222
+ Never raises.
223
+ """
224
+ out: Dict[str, bool] = {}
225
+ for name in ("torch", "torch.distributed", "deepspeed", "megatron", "flax", "tensorflow", "jax", "ray"):
226
+ try:
227
+ __import__(name)
228
+ out[name] = True
229
+ except Exception: # noqa: BLE001
230
+ out[name] = False
231
+ return out