Repository navigation
Expand file tree
/
Copy pathLLM_execution.py
More file actions
836 lines (709 loc) · 33.2 KB
/
Copy pathLLM_execution.py
File metadata and controls
836 lines (709 loc) · 33.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
from __future__ import annotations
import math
import os
from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, Tuple, Optional, List, Set
import simulate_LLM
from astrasim_lib import run_astra_simulation_only_onepath
from simulate_LLM import Graph
def _env_flag(name: str) -> bool:
value = os.environ.get(name)
if value is None:
return False
normalized = value.strip().lower()
return normalized not in {"", "0", "false", "no"}
class ExecutionMode(Enum):
ANALYTICAL = "analytical"
HYBRID = "hybrid"
FULL_ASTRASIM_HIERARCHICAL = "full_astrasim_hierarchical"
FULL_ASTRASIM_FLATTENED = "full_astrasim_flattened"
@dataclass
class ExecutionResult:
total_time: float
graph_root: Any
mode: ExecutionMode
@dataclass
class TransformerTimings:
forward: float
backward: float
class PipelineGraphFlattener:
"""Expand pipeline transformer nodes into explicit tensor-parallel subgraphs."""
def __init__(
self,
pipeline_graph: Graph,
transformer_graph: Graph,
) -> None:
if transformer_graph is None:
raise ValueError("Transformer graph is required for flattening")
transformer_cfg = getattr(transformer_graph, "transformer_cfg", None) or {}
gemm_entries = transformer_cfg.get("gemms")
if not gemm_entries:
raise ValueError("Transformer GEMM template is missing")
self.pipeline_graph = pipeline_graph
self.transformer_graph = transformer_graph
self._gemm_entries = list(gemm_entries)
par_degree = transformer_graph.tp * transformer_graph.cp
self._par_degree = max(1, int(par_degree))
self._zero_stage = int(getattr(transformer_graph, "misc_metadata", {}).get("dp_zero_stage", 0))
# Track original ZeRO-3 transformer gather edges -> per-rank clones
self._clone_cache: Dict[int, Any] = {}
self._op_id_counter: int = 0
def _should_shard_zero3_transformer(self, edge: Any) -> bool:
if self._par_degree <= 1 or self._zero_stage < 3:
return False
if isinstance(edge, simulate_LLM.Edge):
if getattr(edge, "tp_shard", False):
return True
return False
def _ensure_zero3_per_rank_edges(
self,
edge: simulate_LLM.Edge,
rank_heads = None,
rank_tails = None,
hw_ids = None
) -> List[simulate_LLM.Edge]:
transformer_mode = ""
per_rank_edges: List[simulate_LLM.Edge] = []
if rank_tails and rank_heads:
# this is used in expand_transformer_node
# use them as anchors
wire_anchors = rank_tails
direction = getattr(edge, "direction", None)
if direction and str(direction).lower() == "backward":
wire_anchors = rank_heads
iterable = wire_anchors
transformer_mode = True
elif hw_ids:
# this is used for softmax embedding edges for raw cloning.
iterable = hw_ids
transformer_mode = False
if transformer_mode == "":
raise Exception("Invalid _ensure_zero3_per_rank_edges call. At least one of (rank_tails,rank_heads) or (hw_ids) must be provided.")
for r, item in enumerate(iterable):
base_bytes = getattr(edge, "comm_size_bytes", 0)
per_rank_bytes = int(base_bytes)
gather_edge = simulate_LLM.Edge(
name=f"{edge.name}_rank{r}",
op_id=self._next_op_id(),
duration=0,
is_dp=True,
comm_size_bytes=per_rank_bytes,
comm_type=getattr(edge, "comm_type", None),
participants=getattr(edge, "participants", 0),
comm_interconnect_type=getattr(edge, "comm_interconnect_type", None),
)
gather_edge.tp_rank = r
gather_edge.stage_id = getattr(edge, "stage_id", None)
gather_edge.micro_batch_index = getattr(edge, "micro_batch_index", None)
gather_edge.layer_index = getattr(edge, "layer_index", None)
gather_edge.direction = getattr(edge, "direction", None)
if transformer_mode:
if getattr(item, "hw_id", None) is not None:
gather_edge.local_hw_id = item.hw_id
else:
gather_edge.local_hw_id = getattr(item, "local_hw_id", None)
# has the same children as item
for child in getattr(item, "children", []):
gather_edge.add_child(child)
else:
gather_edge.local_hw_id = item
# we cannot attach children here as we don't have them yet.
per_rank_edges.append(gather_edge)
return per_rank_edges
def build(self, root: Any) -> Any:
"""Return a flattened clone of the provided pipeline root."""
if root is None:
raise ValueError("Pipeline root is required for flattening")
return self._clone(root)
def _clone(self, obj: Any) -> Any:
if obj is None:
return None
obj_id = id(obj)
if obj_id in self._clone_cache:
return self._clone_cache[obj_id]
if isinstance(obj, simulate_LLM.Node):
base_name = str(getattr(obj, "name", "") or "")
if base_name.startswith("transformer_layer"):
expanded = self._expand_transformer_node(obj)
self._clone_cache[obj_id] = expanded
return expanded
if "linear_softmax" in obj.name:
# for linear softmax we need to carefully look at hw id to choose.
cloned = simulate_LLM.Node(
obj.name,
self._next_op_id(),
self._hw_id_for_rank(obj.hw_id, 0),
obj.duration,
fwd=obj.fwd,
)
else:
cloned = simulate_LLM.Node(
obj.name,
self._next_op_id(),
obj.hw_id,
obj.duration,
fwd=obj.fwd,
)
# following _expand_transformer_node logic, we need to find siblings that are zero3 tp_shard=True.
zero3_attachments: List[simulate_LLM.Edge] = []
for parent in getattr(obj, "parents", []):
for sibling in getattr(parent, "children", []):
if sibling is obj:
continue
if self._should_shard_zero3_transformer(sibling):
zero3_attachments.append(sibling)
hw_ids = []
for tp_rank in range(self._par_degree):
hw_ids.append(self._hw_id_for_rank(obj.hw_id, tp_rank))
per_rank_edges: List[simulate_LLM.Edge] = []
for zero3_attachment in zero3_attachments:
per_rank_edges = self._ensure_zero3_per_rank_edges(zero3_attachment, rank_heads=None, rank_tails=None, hw_ids=hw_ids)
self._clone_cache[id(zero3_attachment)] = per_rank_edges
self._clone_cache[obj_id] = cloned
self._copy_metadata(obj, cloned)
for child in getattr(obj, "children", []):
child_clone = self._clone(child)
if child_clone is not None:
if per_rank_edges:
for per_rank_edge in per_rank_edges:
self._attach(per_rank_edge, child_clone)
self._attach(cloned, child_clone)
return cloned
if isinstance(obj, simulate_LLM.Edge):
cloned_edge = simulate_LLM.Edge(
obj.name,
self._next_op_id(),
obj.duration,
is_dp=getattr(obj, "is_dp", False),
comm_size_bytes=getattr(obj, "comm_size_bytes", 0),
comm_type=getattr(obj, "comm_type", None),
participants=getattr(obj, "participants", 1),
comm_interconnect_type=getattr(obj, "comm_interconnect_type", None),
)
cloned_edge.local_hw_id = getattr(obj, "local_hw_id", None)
self._clone_cache[obj_id] = cloned_edge
self._copy_metadata(obj, cloned_edge)
for child in getattr(obj, "children", []):
child_clone = self._clone(child)
if child_clone is not None:
self._attach(cloned_edge, child_clone)
return cloned_edge
if isinstance(obj, simulate_LLM.Data_batch):
cloned_batch = simulate_LLM.Data_batch(obj.name, obj.batch_id, obj.duration)
self._clone_cache[obj_id] = cloned_batch
for child in getattr(obj, "children", []):
child_clone = self._clone(child)
if child_clone is not None:
self._attach(cloned_batch, child_clone)
return cloned_batch
raise TypeError(f"Unsupported graph element type: {type(obj)!r}")
def _expand_transformer_node(self, node: simulate_LLM.Node) -> Tuple[Any, ...]:
node_id = id(node)
if node_id in self._clone_cache:
cached_entry = self._clone_cache[node_id]
if isinstance(cached_entry, (list, tuple)):
return tuple(cached_entry)
stage_id = getattr(node, "stage_id", node.hw_id)
micro_batch = getattr(node, "micro_batch_index", None)
layer_index = getattr(node, "layer_index", None)
direction = getattr(node, "direction", "forward" if node.fwd else "backward")
rank_heads: List[Any] = []
rank_tails: List[Any] = []
for tp_rank in range(self._par_degree):
previous: Optional[Any] = None
head: Optional[Any] = None
hw_id = self._hw_id_for_rank(stage_id, tp_rank)
gemm_iterable = self._gemm_entries
if direction == "backward":
gemm_iterable = list(reversed(self._gemm_entries))
for gemm_idx, entry in enumerate(gemm_iterable):
entry_name = entry.get("name", f"g{gemm_idx}")
cfg = entry.get(direction, {})
duration = cfg.get("duration")
if duration is None:
raise ValueError(
f"Missing duration for transformer entry '{entry_name}' in direction '{direction}'"
)
gemm_node = simulate_LLM.Node(
name=self._format_gemm_name(entry_name, direction, micro_batch, layer_index, tp_rank),
op_id=self._next_op_id(),
hw_id=hw_id,
duration=duration,
fwd=(direction == "forward"),
)
gemm_node.stage_id = stage_id
gemm_node.tp_rank = tp_rank
gemm_node.micro_batch_index = micro_batch
gemm_node.layer_index = layer_index
gemm_node.direction = direction
if previous is not None:
previous.add_child(gemm_node)
previous = gemm_node
if head is None:
head = gemm_node
for comm_key in cfg.get("comm_keys", []):
comm_edge = self._create_transformer_comm_edge(
comm_key,
hw_id,
stage_id,
micro_batch,
layer_index,
direction,
tp_rank,
)
previous.add_child(comm_edge)
previous = comm_edge
if head is None:
raise ValueError("Transformer expansion produced no GEMM nodes")
rank_heads.append(head)
rank_tails.append(previous or head)
dp_children: List[Any] = []
other_children: List[Any] = []
zero3_attachments: List[simulate_LLM.Edge] = []
for child in getattr(node, "children", []):
comm_type = getattr(child, "comm_interconnect_type", None)
if comm_type == "dp":
dp_children.append(child)
else:
other_children.append(child)
for parent in getattr(node, "parents", []):
for sibling in getattr(parent, "children", []):
if sibling is node:
continue
if self._should_shard_zero3_transformer(sibling):
zero3_attachments.append(sibling)
# Keep the main trunk pointing to the per-rank compute tails.
downstream_parents: List[Any] = list(rank_tails)
# Attach DP collectives as side branches from the compute tails, without
# reparenting the trunk. This preserves the true cross-layer pipeline
# edge between compute nodes for ET conversion.
for child in dp_children:
if self._should_shard_zero3_transformer(child):
per_rank_edges = self._ensure_zero3_per_rank_edges(child, rank_heads, rank_tails)
child_clone = self._clone(child)
if child_clone is None:
continue
self._attach(rank_tails[0], child_clone) # only attach to the first tail for DP collectives
# Non-DP edges (e.g., cross_layer) stay on the trunk so (parent, target)
# compute → compute pipeline edges remain visible.
for child in other_children:
# Special-case marked cross_layer edges (set in original graph):
# create one per TP rank and wire tail[r] -> cross_layer_r -> next_head[r].
is_pipeline_edge = False
if isinstance(child, simulate_LLM.Edge):
comm_type = getattr(child, "comm_type", None)
if comm_type == "pipeline":
is_pipeline_edge = True
if is_pipeline_edge:
# Determine per-rank byte size (ceil split)
try:
total_bytes = int(getattr(child, "comm_size_bytes", 0))
except Exception:
total_bytes = 0
per_rank_bytes = int(math.ceil(float(total_bytes) / float(max(1, self._par_degree))))
# Clone the original targets of this pipeline edge
target_clones: List[Any] = []
for tgt in getattr(child, "children", []):
tgt_clone = self._clone(tgt)
if tgt_clone is None:
continue
target_clones.append(tgt_clone)
if not target_clones:
# No downstream target; skip safely
continue
# For each TP rank, create its own pipeline edge and connect
for r, tail in enumerate(rank_tails):
# Create rank-specific pipeline edge
edge_obj = simulate_LLM.Edge(
name=f"{getattr(child, 'name', '')}_rank{r}",
op_id=self._next_op_id(),
duration=0,
is_dp=False,
comm_size_bytes=per_rank_bytes,
comm_type="pipeline",
participants=2,
comm_interconnect_type="lp",
)
edge_obj.is_cross_layer = True
tail.add_child(edge_obj)
# Also anchor to the compute node (two parents) for mapping clarity
last_compute = rank_heads[r]
# Find the nearest compute ancestor for this rank: walk back from tail if needed
compute_anchor = None
cur = tail
visited_ids = set()
while cur is not None and id(cur) not in visited_ids:
visited_ids.add(id(cur))
if isinstance(cur, simulate_LLM.Node):
compute_anchor = cur
break
parents = getattr(cur, "parents", [])
cur = parents[-1] if parents else None
if compute_anchor is not None and compute_anchor is not edge_obj:
if compute_anchor != tail:
compute_anchor.add_child(edge_obj)
# Connect to each cloned target, aligning ranks where possible
for tgt_clone in target_clones:
if isinstance(tgt_clone, (list, tuple)):
# Map by identity index when available
idx = r % len(tgt_clone)
edge_obj.add_child(tgt_clone[idx])
else:
edge_obj.add_child(tgt_clone)
continue
# Default path for non-pipeline children
child_clone = self._clone(child)
if child_clone is None:
continue
self._attach(downstream_parents, child_clone)
for zero3_edge in zero3_attachments:
per_rank_edges = self._ensure_zero3_per_rank_edges(zero3_edge, rank_heads, rank_tails, hw_ids=None)
self._clone_cache[id(zero3_edge)] = per_rank_edges
heads_tuple = tuple(rank_heads)
self._clone_cache[node_id] = heads_tuple
return heads_tuple
def _create_transformer_comm_edge(
self,
comm_key: str,
hw_id: int,
stage_id: int,
micro_batch: Optional[int],
layer_index: Optional[int],
direction: str,
tp_rank: int,
) -> simulate_LLM.Edge:
comm_info = self.transformer_graph.comm_metadata.get(comm_key, {})
is_dp_edge = comm_info.get("interconnect_type") == "dp"
comm_edge = self.transformer_graph.create_comm_edge(
name=comm_key,
op_id=self._next_op_id(),
comm_key=comm_key,
is_dp=is_dp_edge,
local_hw_id=hw_id,
)
comm_edge.stage_id = stage_id
comm_edge.micro_batch_index = micro_batch
comm_edge.layer_index = layer_index
comm_edge.direction = direction
comm_edge.tp_rank = tp_rank
return comm_edge
def _copy_metadata(self, source: Any, target: Any) -> None:
for attr in (
"micro_batch_index",
"layer_index",
"direction",
"stage_id",
"tp_rank",
):
if hasattr(source, attr):
setattr(target, attr, getattr(source, attr))
def _attach(self, parent: Any, child: Any) -> None:
if parent is None or child is None:
return
if isinstance(parent, (list, tuple)):
for item in parent:
self._attach(item, child)
return
if isinstance(child, (list, tuple)):
for item in child:
self._attach(parent, item)
return
parent.add_child(child)
def _format_gemm_name(
self,
base_name: str,
direction: str,
micro_batch: Optional[int],
layer_index: Optional[int],
tp_rank: int,
) -> str:
return f"{base_name}_{direction}_mb{micro_batch}_l{layer_index}_rank{tp_rank}"
def _next_op_id(self) -> int:
self._op_id_counter += 1
return self._op_id_counter
def _hw_id_for_rank(self, stage_id: int, tp_rank: int) -> int:
stage_int = int(stage_id) if stage_id is not None else 0
return stage_int * self._par_degree + tp_rank
class LLMExecutionDispatcher:
def __init__(
self,
time_calc: TimeCalculationLLM, # somehow this works? TODO: fix this at some point so its not annotated by IDE.
pipeline_graph: Graph,
pipeline_root: Any,
interconnect_params: Dict[str, Tuple[float, float]],
transformer_graph: Optional[Graph] = None,
transformer_forward_root: Optional[Any] = None,
transformer_backward_root: Optional[Any] = None,
) -> None:
self.time_calc = time_calc
self.pipeline_graph = pipeline_graph
self.pipeline_root = pipeline_root
self.interconnect_params = interconnect_params
self.transformer_graph = transformer_graph
self.transformer_forward_root = transformer_forward_root
self.transformer_backward_root = transformer_backward_root
self.flattened_root: Optional[Any] = None
def run(self, mode: ExecutionMode) -> ExecutionResult:
if mode == ExecutionMode.ANALYTICAL:
return self._run_pipeline_with_analytical_comm(ExecutionMode.ANALYTICAL)
if mode == ExecutionMode.HYBRID:
return self._run_hybrid()
if mode == ExecutionMode.FULL_ASTRASIM_HIERARCHICAL:
return self._run_full_astrasim_hierarchical()
if mode == ExecutionMode.FULL_ASTRASIM_FLATTENED:
return self._run_full_astrasim_flattened()
raise ValueError(f"Unsupported execution mode: {mode}")
def _run_pipeline_with_analytical_comm(self, declared_mode: ExecutionMode) -> ExecutionResult:
if declared_mode == ExecutionMode.HYBRID:
filename = "/hybrid_graph"
timed_root = self.pipeline_root
else: # must be "ANALYTICAL"
filename = "/analytical_graph"
timed_root = self.pipeline_graph.convert_comm_sizes_to_times(
self.pipeline_root,
self.time_calc.network_model,
self.interconnect_params,
)
generate_graphs = _env_flag("DEEPFLOW_VISUALIZE_GRAPHS")
if generate_graphs:
self.pipeline_graph.save_graph(
self.pipeline_root,
self.time_calc.output_dir,
filename,
)
# Persist timed root for any downstream consumer
self.pipeline_root = timed_root
total_time = self.pipeline_graph.simulate(timed_root)
return ExecutionResult(total_time=total_time, graph_root=timed_root, mode=declared_mode)
def _run_hybrid(self) -> ExecutionResult:
generate_graphs = _env_flag("DEEPFLOW_VISUALIZE_GRAPHS")
if generate_graphs:
self.transformer_graph.save_graph(
self.transformer_forward_root,
self.time_calc.output_dir,
"/hybrid_graph_transformer",
)
transformer_time = self._run_transformer_astrasim(ExecutionMode.HYBRID)
if generate_graphs:
self.transformer_graph.save_graph(
self.transformer_forward_root,
self.time_calc.output_dir,
"/hybrid_graph_transformer",
)
if transformer_time is not None:
self._apply_transformer_time(transformer_time)
return self._run_pipeline_with_analytical_comm(ExecutionMode.HYBRID)
def _run_full_astrasim_hierarchical(self) -> ExecutionResult:
transformer_time = self._run_transformer_astrasim(ExecutionMode.FULL_ASTRASIM_HIERARCHICAL)
if transformer_time is not None:
self._apply_transformer_time(transformer_time)
dp_count = getattr(self.time_calc, "dp", 1) or 1
if not self.pipeline_root:
raise RuntimeError("Pipeline graph root is not available for AstraSim execution")
# Use hierarchical artifact directory when persisting artifacts
artifact_dir = self.time_calc.output_dir
if self.time_calc.persist_astrasim_artifacts:
artifact_dir = os.path.join(self.time_calc.output_dir, "astra_hier")
run_kwargs = {
"persist_artifacts": self.time_calc.persist_astrasim_artifacts,
}
run_type = str(getattr(getattr(self.time_calc, "model", None), "run_type", "training")).lower()
effective_dp = 1 if run_type == "inference" else max(1, getattr(self.time_calc, "dp", 1))
if run_type == "inference":
run_kwargs["dp_override"] = 1
if _env_flag("DEEPFLOW_VISUALIZE_GRAPHS") and self.pipeline_root is not None:
self.pipeline_graph.save_graph(
self.pipeline_root,
self.time_calc.output_dir,
"/pipeline_graph_hierarchical",
)
per_rank_sec, max_sec = run_astra_simulation_only_onepath(
self.pipeline_root,
self.time_calc,
artifact_dir,
**run_kwargs,
)
self.time_calc.pipeline_astrasim_per_rank = per_rank_sec
self.time_calc.pipeline_astrasim_time = max_sec
if max_sec <= 0:
raise RuntimeError("AstraSim pipeline execution returned non-positive duration")
return ExecutionResult(total_time=max_sec, graph_root=self.pipeline_root, mode=ExecutionMode.FULL_ASTRASIM_HIERARCHICAL)
def _run_full_astrasim_flattened(self) -> ExecutionResult:
if not self.pipeline_root:
raise RuntimeError("Pipeline graph root is not available for flattening")
if not self.transformer_graph:
raise RuntimeError("Transformer graph metadata is required for flattening")
flattener = PipelineGraphFlattener(
pipeline_graph=self.pipeline_graph,
transformer_graph=self.transformer_graph,
)
if _env_flag("DEEPFLOW_VISUALIZE_GRAPHS") and self.pipeline_root is not None:
self.pipeline_graph.save_graph(
self.pipeline_root,
self.time_calc.output_dir,
"/pipeline_graph_pre_flatten",
)
flattened_root = flattener.build(self.pipeline_root)
if flattened_root is None:
raise RuntimeError("Pipeline flattening produced an empty graph")
self.time_calc.flattened_pipeline_root = flattened_root
if _env_flag("DEEPFLOW_VISUALIZE_GRAPHS") and self.pipeline_root is not None:
self.pipeline_graph.save_graph(
flattened_root,
self.time_calc.output_dir,
"/pipeline_graph_post_flatten",
)
self.pipeline_root = flattened_root
# output_dir = "./astra_flattened_graph"
# os.makedirs(output_dir, exist_ok=True)
# base_path = os.path.join(output_dir, "pipeline_flattened")
# dot = visualize_graph(flattened_root, filename=base_path)
# try:
# dot.render(base_path, format="png", cleanup=True)
# except Exception as exc: # pragma: no cover - visualization best-effort
# print(f"[WARN] Failed to render flattened pipeline graph: {exc}")
unique_hw_ids = self._collect_hw_ids(flattened_root)
if not unique_hw_ids:
raise RuntimeError("Flattened pipeline graph exposes no compute nodes with hardware IDs")
# Use flattened artifact directory when persisting artifacts
artifact_dir = self.time_calc.output_dir
if self.time_calc.persist_astrasim_artifacts:
artifact_dir = os.path.join(self.time_calc.output_dir, "astra_flat")
run_kwargs = {
"persist_artifacts": self.time_calc.persist_astrasim_artifacts,
}
run_type = str(getattr(getattr(self.time_calc, "model", None), "run_type", "training")).lower()
effective_dp = 1 if run_type == "inference" else max(1, getattr(self.time_calc, "dp", 1))
if run_type == "inference":
run_kwargs["dp_override"] = 1
per_rank_sec, max_sec = run_astra_simulation_only_onepath(
flattened_root,
self.time_calc,
artifact_dir,
**run_kwargs,
)
if not per_rank_sec:
raise RuntimeError("AstraSim flattened execution returned no per-rank timings")
expected_rank_count = effective_dp * len(unique_hw_ids)
# Special case: If expected rank count is 1, then 2 is fine, but we prune the extra result
# this is done, since astrasim backend only supports >1 ranks, so we generate extra fake result for that case.
if expected_rank_count == 1:
if len(per_rank_sec) > 2:
raise RuntimeError(
"AstraSim rank count mismatch for flattened execution: "
f"expected {expected_rank_count}, got {len(per_rank_sec)}"
)
per_rank_sec = per_rank_sec[:1]
if len(per_rank_sec) != expected_rank_count:
raise RuntimeError(
"AstraSim rank count mismatch for flattened execution: "
f"expected {expected_rank_count}, got {len(per_rank_sec)}"
)
if max_sec <= 0:
raise RuntimeError("AstraSim flattened execution returned non-positive duration")
self.time_calc.pipeline_astrasim_per_rank = per_rank_sec
self.time_calc.pipeline_astrasim_time = max_sec
self.time_calc.flattened_astrasim_per_rank = per_rank_sec
self.time_calc.flattened_astrasim_total = max_sec
return ExecutionResult(
total_time=max_sec,
graph_root=flattened_root,
mode=ExecutionMode.FULL_ASTRASIM_FLATTENED,
)
def _collect_hw_ids(self, root: Any) -> Set[int]:
visited: Set[int] = set()
hw_ids: Set[int] = set()
def enqueue_children(obj: Any) -> None:
for child in getattr(obj, "children", []):
stack.append(child)
stack: List[Any]
if isinstance(root, (list, tuple)):
stack = list(root)
else:
stack = [root]
while stack:
obj = stack.pop()
obj_id = id(obj)
if obj_id in visited:
continue
visited.add(obj_id)
if isinstance(obj, simulate_LLM.Node):
hw_id = getattr(obj, "hw_id", None)
if hw_id is not None and hw_id >= 0:
hw_ids.add(int(hw_id))
enqueue_children(obj)
return hw_ids
def _run_transformer_astrasim(self, mode: ExecutionMode) -> Optional[TransformerTimings]:
del mode # mode currently unused but kept for signature consistency
# Use hierarchical artifact directory when persisting artifacts for transformer simulation
artifact_dir = self.time_calc.output_dir
artifact_dir_fwd = artifact_dir
artifact_dir_bwd = artifact_dir
os.makedirs(artifact_dir, exist_ok=True)
if self.time_calc.persist_astrasim_artifacts:
artifact_dir = os.path.join(self.time_calc.output_dir, "astra_hier")
artifact_dir_fwd = os.path.join(artifact_dir, "fwd")
artifact_dir_bwd = os.path.join(artifact_dir, "bwd")
os.makedirs(artifact_dir_fwd, exist_ok=True)
os.makedirs(artifact_dir_bwd, exist_ok=True)
fwd_per_rank = None
bwd_per_rank = None
fwd_max = 0
bwd_max = 0
if self.transformer_forward_root:
fwd_per_rank, fwd_max = run_astra_simulation_only_onepath(
self.transformer_forward_root,
self.time_calc,
artifact_dir_fwd,
dp_override=1,
persist_artifacts=self.time_calc.persist_astrasim_artifacts,
)
if self.transformer_backward_root:
bwd_per_rank, bwd_max = run_astra_simulation_only_onepath(
self.transformer_backward_root,
self.time_calc,
artifact_dir_bwd,
dp_override=1,
persist_artifacts=self.time_calc.persist_astrasim_artifacts,
)
self.time_calc.transformer_astrasim_per_rank_forward = fwd_per_rank
self.time_calc.transformer_astrasim_per_rank_backward = bwd_per_rank
self.time_calc.transformer_astrasim_time_forward = fwd_max
self.time_calc.transformer_astrasim_time_backward = bwd_max
if fwd_max < 0 or bwd_max < 0:
raise RuntimeError("AstraSim transformer execution returned non-positive duration")
return TransformerTimings(forward=fwd_max, backward=bwd_max)
def _apply_transformer_time(self, timings: TransformerTimings) -> None:
if timings.forward < 0 or timings.backward < 0:
raise ValueError("AstraSim transformer times must be positive")
comp_times = getattr(self.pipeline_graph, "comp_times", None)
if isinstance(comp_times, dict):
if "transformer_f" in comp_times:
comp_times["transformer_f"] = timings.forward
if "transformer_b" in comp_times:
comp_times["transformer_b"] = timings.backward
visited: Set[int] = set()
roots: List[Any]
if isinstance(self.pipeline_root, (list, tuple)):
roots = list(self.pipeline_root)
else:
roots = [self.pipeline_root]
for root in roots:
self._assign_transformer_durations(root, visited, timings.forward, timings.backward)
def _assign_transformer_durations(self, node: Any, visited: Set[int], forward_value: float, backward_value: float) -> None:
if node is None:
return
node_id = id(node)
if node_id in visited:
return
visited.add(node_id)
if isinstance(node, simulate_LLM.Node):
base_name = str(getattr(node, "name", "") or "")
if base_name.startswith("transformer_layer"):
if getattr(node, "fwd", True):
node.duration = forward_value
else:
node.duration = backward_value
for child in getattr(node, "children", []):
self._assign_transformer_durations(child, visited, forward_value, backward_value)