Skip to content

Commit a2c4505

Browse files
Merge branch 'main' into te-v2-reset-transform
2 parents b08f65a + 73d321b commit a2c4505

5 files changed

Lines changed: 172 additions & 54 deletions

File tree

thunder/dynamo/report.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import copy
1111
from itertools import chain
1212
from looseversion import LooseVersion
13+
import shutil
1314

1415
import torch
1516
from thunder.core.pytree import tree_flatten
@@ -1393,3 +1394,111 @@ def save_failing_repros(
13931394
report.write_repro(
13941395
repros_folder, compile_fn, extra_comment_str=comment, check_consistency=check_consistency
13951396
)
1397+
1398+
1399+
def create_folder(folder_path: str | PathLike, force_overwrite: bool = False):
1400+
folder_path = Path(folder_path)
1401+
1402+
if folder_path.exists():
1403+
if not folder_path.is_dir():
1404+
raise RuntimeError(f"{folder_path} exists and is not a directory.")
1405+
1406+
if force_overwrite:
1407+
shutil.rmtree(folder_path)
1408+
else:
1409+
raise RuntimeError(f"Folder {folder_path} already exists. Use force_overwrite=True to overwrite.")
1410+
1411+
folder_path.mkdir(parents=True, exist_ok=False)
1412+
1413+
1414+
def save_thunderfx_repros(
1415+
fn: Callable,
1416+
folder_path: str | PathLike,
1417+
*,
1418+
use_benchmark: bool = False,
1419+
check_runnability: bool = False,
1420+
save_fusion: bool = False,
1421+
save_trace: bool = False,
1422+
stream: TextIO = sys.stdout,
1423+
force_overwrite: bool = False,
1424+
**compile_kwargs,
1425+
):
1426+
"""
1427+
Saves reproduction scripts for ThunderFX subgraphs.
1428+
1429+
This function:
1430+
1. Creates a folder structure to organize the repros
1431+
.
1432+
└── graph0
1433+
├── fusion_reports
1434+
│ ├── graph0_thunder_0_nvFusion0_forward_repro_nvfuser.py
1435+
│ ├── graph0_thunder_0_nvFusion1_forward_repro_nvfuser.py
1436+
│ ├── graph0_thunder_0_nvFusion2_backward_repro_nvfuser.py
1437+
├── graph0_thunder_0_bwd_trace.py
1438+
├── graph0_thunder_0_fwd_trace.py
1439+
└── graph0_thunder_0.py
1440+
1441+
2. For each Thunder FX graph and its subgraphs:
1442+
- Checks runnability if requested
1443+
- Saves benchmark or repro scripts
1444+
- Saves trace information if requested
1445+
- Saves nvFusion repros if requested
1446+
1447+
Args:
1448+
fn: The callable to analyze
1449+
folder_path: Path to save repros to
1450+
use_benchmark: If True, saves benchmark scripts instead of repros
1451+
check_runnability: If True, checks if graphs can run with Thunder
1452+
save_fusion: If True, saves nvFusion repros
1453+
save_trace: If True, saves trace information
1454+
stream: Stream to write output log informationto
1455+
force_overwrite: If True, overwrites existing folder at folder_path
1456+
**compile_kwargs: Keyword arguments for Thunder and torch.compile
1457+
1458+
Returns:
1459+
A wrapped function that saves repros when called with inputs
1460+
"""
1461+
from thunder.dynamo.utils import get_torch_compile_kwargs
1462+
1463+
folder_path = Path(folder_path)
1464+
create_folder(folder_path, force_overwrite)
1465+
torch_compile_kwargs = get_torch_compile_kwargs(**compile_kwargs)
1466+
thunder_jit_kwargs = {k: v for k, v in compile_kwargs.items() if k not in torch_compile_kwargs}
1467+
thunderjit = ThunderCompileSpecification(**thunder_jit_kwargs)
1468+
1469+
def inner_fn(*args, **kwargs):
1470+
thunder_fxgraph_reports = get_thunder_fxgraph_reports(fn, stream=stream, **compile_kwargs)(*args, **kwargs)
1471+
for thunder_fxgraph_report in thunder_fxgraph_reports:
1472+
graph_folder = folder_path / thunder_fxgraph_report.graph_name
1473+
graph_folder.mkdir(exist_ok=True, parents=True)
1474+
for split_report in thunder_fxgraph_report.subgraph_reports:
1475+
if check_runnability or save_trace or save_fusion:
1476+
try:
1477+
split_report.create_fusion_reports()
1478+
except Exception as e:
1479+
stream.write(f"Failed to run the {split_report.graph_name} using Thunder with exception: {e}\n")
1480+
split_report.write_repro(
1481+
graph_folder, thunderjit, file_name=f"failed_{split_report.graph_name}.py"
1482+
)
1483+
continue
1484+
else:
1485+
stream.write(f"Successfully ran the {split_report.graph_name} using Thunder\n")
1486+
if use_benchmark:
1487+
split_report.write_benchmark(graph_folder, thunderjit, WallTime)
1488+
else:
1489+
split_report.write_repro(graph_folder, thunderjit)
1490+
if save_trace:
1491+
with open(graph_folder / f"{split_report.graph_name}_fwd_trace.py", "w") as f:
1492+
f.write(str(split_report.fwd_trc))
1493+
with open(graph_folder / f"{split_report.graph_name}_bwd_trace.py", "w") as f:
1494+
f.write(str(split_report.bwd_trc))
1495+
if save_fusion:
1496+
fusion_folder = graph_folder / "fusion_reports"
1497+
fusion_folder.mkdir(exist_ok=True, parents=True)
1498+
for fusion_report in split_report.fusion_reports:
1499+
if use_benchmark:
1500+
fusion_report.write_nvfuser_benchmark(fusion_folder, WallTime)
1501+
else:
1502+
fusion_report.write_nvfuser_repro(fusion_folder)
1503+
1504+
return inner_fn

thunder/executors/nvfuserex_impl.py

Lines changed: 36 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@
7171
from thunder.extend import FUEL_LEVEL, FusionExecutor, register_executor
7272
from thunder.executors.nvfuserex import nvfuser_version
7373

74+
75+
DTENSOR_SUPPORTED_VERSION = LooseVersion("0.2.28")
76+
if nvfuser_version() >= DTENSOR_SUPPORTED_VERSION:
77+
import nvfuser_direct as nvfd
78+
from nvfuser_direct import FusionDefinition as DirectFusionDefinition
79+
7480
# NOTE This impl file is here because nvFuser may not be available, so it's imported conditionally
7581
# by nvfuserex.py when nvFuser is available.
7682
import nvfuser
@@ -241,41 +247,34 @@ def get_translator(bsym: BoundSymbol) -> Callable:
241247
return _translation_map[bsym.sym.id]
242248

243249

244-
class MultiDeviceFusionDefinition(FusionDefinition):
245-
def __init__(self, define_fn: Callable[[FusionDefinition], None], in_dtensors: list[DTensorProxy], max_length: int):
246-
super().__init__(max_length=max_length)
247-
self._in_dtensors = in_dtensors
248-
self._define_fn = define_fn
250+
def register_dtensor_supported(prim_id: int, fn: Callable, checker_fn: Callable) -> None:
251+
if nvfuser_version() < DTENSOR_SUPPORTED_VERSION:
252+
# Only register dtensor ops if supported version is available.
253+
return
249254

250-
def definition(self) -> None:
251-
self._define_fn(self)
255+
register_supported(prim_id, fn, checker_fn)
252256

253-
def _find_tensor_by_index(self, index: int) -> nvfuser.Tensor:
254-
for t in self.sched.tensors():
255-
if t.index == index:
256-
return t
257-
return None
258257

259-
def multidevice_schedule(self) -> None:
260-
for in_tensor_index, in_dtensor in zip(self.inputs(), self._in_dtensors):
261-
in_tensor = self._find_tensor_by_index(in_tensor_index)
258+
def multidevice_schedule(fd: FusionDefinition, in_dtensors: list[Proxy]) -> None:
259+
for in_tv, in_dtensor in zip(fd.fusion.inputs(), in_dtensors):
260+
assert isinstance(in_dtensor, DTensorProxy)
261+
# Set the device mesh.
262+
assert in_dtensor.device_mesh.ndim == 1, "nvFuser's Python API only supports 1D meshes."
263+
mesh = nvfd.multidevice.DeviceMesh(in_dtensor.device_mesh.mesh.tolist())
262264

263-
# Set the device mesh.
264-
utils.check(in_dtensor.device_mesh.ndim == 1, lambda: "nvFuser's Python API only supports 1D meshes.")
265-
mesh = nvfuser.DeviceMesh(in_dtensor.device_mesh.mesh.tolist())
265+
in_tv.set_device_mesh(mesh)
266266

267-
self.sched._set_device_mesh(in_tensor, mesh)
267+
assert len(in_dtensor.placements) == 1, "nvFuser's Python API only supports 1D meshes."
268268

269-
# Split and parallelize.
270-
utils.check(len(in_dtensor.placements) == 1, lambda: "nvFuser's Python API only supports 1D meshes.")
271-
# When the mesh is multi-dimensional, iterate through the
272-
# placements in descending order of Placement.dim.
273-
placement: Placement = in_dtensor.placements[0]
274-
if placement.is_shard():
275-
dim = cast(Shard, placement).dim
276-
self.sched.split(in_tensor, dim, mesh.size, False)
277-
self.sched.parallelize(in_tensor, dim, nvfuser.ParallelType.mesh_x)
278-
self.sched.set_allocation_as_loop(in_tensor)
269+
# Split and parallelize.
270+
# When the mesh is multi-dimensional, iterate through the
271+
# placements in descending order of Placement.dim.
272+
placement: Placement = in_dtensor.placements[0]
273+
if placement.is_shard():
274+
dim = cast(Shard, placement).dim
275+
in_tv.split(dim, mesh.size, inner_split=False)
276+
in_tv.axis(dim).parallelize(nvfd.ParallelType.mesh_x)
277+
in_tv.set_allocation_domain(in_tv.get_loop_domain(), new_contiguity=True)
279278

280279

281280
def create_fd(
@@ -376,10 +375,13 @@ def check_dtensor_tracing_and_runtime_metadata(inp):
376375
lambda: "nvfuser: Expected runtime and tracing metadata to be the same for DTensor.",
377376
)
378377

379-
fd = MultiDeviceFusionDefinition(definition, sorted_unique_inputs, max_length=MAX_LENGTH)
378+
fd = DirectFusionDefinition()
380379
# Device may be set in one of the "factory" methods like full, iota, or uniform
381380
# NOTE: This should be called before defining because a factory method may look-up at `_selected_device` while being defined.
382381
fd._selected_device = None
382+
with fd:
383+
definition(fd)
384+
multidevice_schedule(fd, sorted_unique_inputs)
383385
else:
384386
# NOTE nvFuser's default max length is 1024 operations at the time of this writing
385387
# This arbitrarily increases it to 9999
@@ -535,28 +537,10 @@ def __call__(self, *args):
535537
if self.store_inputs:
536538
self.last_inputs = args
537539

538-
if hasattr(fd, "multidevice_schedule"):
540+
if dist.is_available() and any(isinstance(t, torch.distributed.tensor.DTensor) for t in args):
539541
with annotate_for_profile(self.name):
540-
in_tensors = [in_dtensor.to_local() for in_dtensor in args]
541-
out_tensors, out_shardings = fd.execute(
542-
in_tensors,
543-
device=fd._selected_device,
544-
save_repro_inputs=self.save_fake_inputs,
545-
_enable_options=self.enable_options,
546-
_disable_options=self.disable_options,
547-
)
548-
549-
assert len(out_tensors) == len(out_shardings)
550-
out_dtensors: list[DTensor] = []
551-
for out_tensor, out_sharding in zip(out_tensors, out_shardings):
552-
mesh = dist.device_mesh.init_device_mesh("cuda", (out_sharding.mesh.size,))
553-
placements: list[Placement] = []
554-
for parallel_type in [nvfuser.ParallelType.mesh_x]:
555-
axis: int = out_sharding.axis_sharded_on(parallel_type)
556-
placements.append(Replicate() if axis == -1 else Shard(axis))
557-
out_dtensors.append(DTensor.from_local(out_tensor, mesh, placements))
558-
559-
return out_dtensors
542+
output = nvfd.execute_with_dtensors(fd, args)
543+
return output
560544
else:
561545
with annotate_for_profile(self.name):
562546
return fd.execute(
@@ -1917,7 +1901,7 @@ def mul(a: TensorProxy | Number, b: TensorProxy | Number, *, fd: FusionDefinitio
19171901

19181902

19191903
register_supported(PrimIDs.MUL, mul, _elementwise_binary_check)
1920-
register_supported(dtensor_mul_prim.id, mul, _elementwise_binary_check)
1904+
register_dtensor_supported(dtensor_mul_prim.id, mul, _elementwise_binary_check)
19211905

19221906

19231907
def ne(a: TensorProxy | Number, b: TensorProxy | Number, *, fd: FusionDefinition, lc_to_nv_map: dict) -> Any:

thunder/tests/opinfos.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2192,6 +2192,14 @@ def imag_error_generator(op, device, **kwargs):
21922192
elementwise_unary_ops.append(clone_opinfo)
21932193

21942194

2195+
square_opinfo = OpInfo(
2196+
ltorch.square,
2197+
sample_input_generator=elementwise_unary_generator,
2198+
torch_reference=_elementwise_unary_torch(torch.square),
2199+
)
2200+
elementwise_unary_ops.append(square_opinfo)
2201+
2202+
21952203
# Puts all opinfos into the "opinfos" list
21962204
opinfos.extend(elementwise_unary_ops)
21972205

thunder/torch/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2248,6 +2248,25 @@ def threshold_(a: TensorProxy, /, threshold: float, value: float) -> TensorLike:
22482248
_inplace_to_out_of_place[threshold_] = threshold, -1
22492249

22502250

2251+
@torchsymbol(torch.square, is_method=True)
2252+
def square(a):
2253+
if isinstance(dtypes.to_dtype(a), dtypes.bool_):
2254+
a = clang.maybe_convert_to_dtype(a, dtypes.int64)
2255+
return a * a
2256+
2257+
2258+
@torchsymbol(torch.square_, is_method=True, tags=(prims.OpTags.IN_PLACE,))
2259+
def square_(a):
2260+
utils.check(
2261+
not isinstance(dtypes.to_dtype(a), dtypes.bool_),
2262+
lambda: f"Result type of {dtypes.int64} cannot be stored into {dtypes.to_dtype(a)}",
2263+
)
2264+
return _copy_(a, square(a))
2265+
2266+
2267+
_inplace_to_out_of_place[square_] = square, -1
2268+
2269+
22512270
#
22522271
# Elementwise binary operations
22532272
#

thunder/torch/default_torch_ops.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,6 @@
252252
torch.split_with_sizes,
253253
torch.split_with_sizes_copy,
254254
torch.spmm,
255-
torch.square,
256255
torch.squeeze_copy,
257256
torch.sspaddmm,
258257
torch.std_mean,
@@ -548,7 +547,6 @@
548547
torch.Tensor.sparse_dim,
549548
torch.Tensor.sparse_mask,
550549
torch.Tensor.split_with_sizes,
551-
torch.Tensor.square,
552550
torch.Tensor.sspaddmm,
553551
torch.Tensor.stft,
554552
torch.Tensor.subtract,

0 commit comments

Comments
 (0)