|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import importlib.util |
| 4 | +from pathlib import Path |
| 5 | +from typing import Any |
| 6 | + |
| 7 | +import torch |
| 8 | + |
| 9 | + |
| 10 | +class ExportedAutogradRuntime: |
| 11 | + def __init__(self, artifacts_dir: Path, specs: list[dict[str, Any]]) -> None: |
| 12 | + self.artifacts_dir = artifacts_dir |
| 13 | + self.specs = specs |
| 14 | + self.forward_runners = [self._load_runner(spec["forward_module"]) for spec in specs] |
| 15 | + self.backward_runners = [self._load_runner(spec["backward_module"]) for spec in specs] |
| 16 | + |
| 17 | + def run_with_bound_args(self, bound_args: dict[str, Any]) -> Any: |
| 18 | + spec_id = self._select_spec(bound_args) |
| 19 | + spec = self.specs[spec_id] |
| 20 | + runtime_tensors = tuple(bound_args[name] for name in spec["runtime_tensor_names"]) |
| 21 | + if spec["needs_autograd"]: |
| 22 | + result = _CompiledAutogradFunction.apply(self, spec_id, *runtime_tensors) |
| 23 | + if spec["output_kind"] == "list": |
| 24 | + return list(result) |
| 25 | + return result |
| 26 | + return self._run_forward_only(spec_id, runtime_tensors) |
| 27 | + |
| 28 | + def _load_runner(self, module_filename: str | None) -> Any: |
| 29 | + if module_filename is None: |
| 30 | + return None |
| 31 | + module_path = self.artifacts_dir / module_filename |
| 32 | + spec = importlib.util.spec_from_file_location(module_path.stem, module_path) |
| 33 | + module = importlib.util.module_from_spec(spec) |
| 34 | + if spec.loader is None: |
| 35 | + raise RuntimeError(f"Could not load compiled source module {module_path}") |
| 36 | + spec.loader.exec_module(module) |
| 37 | + return module.call |
| 38 | + |
| 39 | + def _run_forward_only(self, spec_id: int, runtime_tensors: tuple[torch.Tensor, ...]) -> Any: |
| 40 | + spec = self.specs[spec_id] |
| 41 | + outputs = self.forward_runners[spec_id](list(runtime_tensors)) |
| 42 | + if not isinstance(outputs, tuple): |
| 43 | + outputs = tuple(outputs) |
| 44 | + user_outputs = outputs[: spec["num_user_outputs"]] |
| 45 | + if spec["output_kind"] == "single": |
| 46 | + return user_outputs[0] |
| 47 | + if spec["output_kind"] == "list": |
| 48 | + return list(user_outputs) |
| 49 | + return tuple(user_outputs) |
| 50 | + |
| 51 | + def _select_spec(self, bound_args: dict[str, Any]) -> int: |
| 52 | + matches = [] |
| 53 | + failures = [] |
| 54 | + for spec_id, spec in enumerate(self.specs): |
| 55 | + reasons = _mismatch_reasons(spec, bound_args) |
| 56 | + if reasons: |
| 57 | + failures.append((spec, reasons)) |
| 58 | + else: |
| 59 | + matches.append(spec_id) |
| 60 | + if len(matches) == 1: |
| 61 | + return matches[0] |
| 62 | + if len(matches) > 1: |
| 63 | + names = ", ".join(self.specs[spec_id]["name"] for spec_id in matches) |
| 64 | + raise RuntimeError( |
| 65 | + f"Ambiguous export_autograd_triton specializations matched: {names}" |
| 66 | + ) |
| 67 | + raise RuntimeError(_format_no_match(bound_args, failures)) |
| 68 | + |
| 69 | + |
| 70 | +class _CompiledAutogradFunction(torch.autograd.Function): |
| 71 | + @staticmethod |
| 72 | + def forward(ctx, runtime: ExportedAutogradRuntime, spec_id: int, *runtime_tensors): |
| 73 | + spec = runtime.specs[spec_id] |
| 74 | + outputs = runtime.forward_runners[spec_id](list(runtime_tensors)) |
| 75 | + if not isinstance(outputs, tuple): |
| 76 | + outputs = tuple(outputs) |
| 77 | + user_outputs = outputs[: spec["num_user_outputs"]] |
| 78 | + residuals = outputs[spec["num_user_outputs"] :] |
| 79 | + named_residuals = tuple(zip(spec["forward_residual_names"], residuals, strict=True)) |
| 80 | + ctx.runtime = runtime |
| 81 | + ctx.spec_id = spec_id |
| 82 | + ctx.runtime_tensor_count = len(runtime_tensors) |
| 83 | + ctx.saved_tensor_residual_names = tuple( |
| 84 | + name for name, residual in named_residuals if isinstance(residual, torch.Tensor) |
| 85 | + ) |
| 86 | + ctx.saved_non_tensor_residuals = { |
| 87 | + name: residual |
| 88 | + for name, residual in named_residuals |
| 89 | + if not isinstance(residual, torch.Tensor) |
| 90 | + } |
| 91 | + ctx.save_for_backward( |
| 92 | + *(residual for _, residual in named_residuals if isinstance(residual, torch.Tensor)) |
| 93 | + ) |
| 94 | + non_differentiable_outputs = tuple( |
| 95 | + output |
| 96 | + for output, is_differentiable in zip( |
| 97 | + user_outputs, |
| 98 | + spec["differentiable_output_mask"], |
| 99 | + strict=True, |
| 100 | + ) |
| 101 | + if isinstance(output, torch.Tensor) and not is_differentiable |
| 102 | + ) |
| 103 | + if non_differentiable_outputs: |
| 104 | + ctx.mark_non_differentiable(*non_differentiable_outputs) |
| 105 | + if spec["output_kind"] == "single": |
| 106 | + return user_outputs[0] |
| 107 | + return tuple(user_outputs) |
| 108 | + |
| 109 | + @staticmethod |
| 110 | + def backward(ctx, *grad_outputs): |
| 111 | + runtime = ctx.runtime |
| 112 | + spec = runtime.specs[ctx.spec_id] |
| 113 | + grad_outputs = tuple( |
| 114 | + grad_output.contiguous() if isinstance(grad_output, torch.Tensor) else grad_output |
| 115 | + for index, grad_output in enumerate(grad_outputs) |
| 116 | + if spec["differentiable_output_mask"][index] |
| 117 | + ) |
| 118 | + backward_runner = runtime.backward_runners[ctx.spec_id] |
| 119 | + if backward_runner is None: |
| 120 | + raise RuntimeError( |
| 121 | + "Selected export_autograd_triton specialization has no backward graph" |
| 122 | + ) |
| 123 | + saved_residuals = dict(ctx.saved_non_tensor_residuals) |
| 124 | + saved_residuals.update( |
| 125 | + zip(ctx.saved_tensor_residual_names, ctx.saved_tensors, strict=True) |
| 126 | + ) |
| 127 | + backward_saved_inputs = tuple( |
| 128 | + saved_residuals[name] for name in spec["backward_saved_input_names"] |
| 129 | + ) |
| 130 | + grads = backward_runner(list(backward_saved_inputs + grad_outputs)) |
| 131 | + if not isinstance(grads, tuple): |
| 132 | + grads = tuple(grads) |
| 133 | + if len(grads) < ctx.runtime_tensor_count: |
| 134 | + grads = grads + (None,) * (ctx.runtime_tensor_count - len(grads)) |
| 135 | + if len(grads) > ctx.runtime_tensor_count: |
| 136 | + grads = grads[: ctx.runtime_tensor_count] |
| 137 | + return (None, None, *grads) |
| 138 | + |
| 139 | + |
| 140 | +def _mismatch_reasons(spec: dict[str, Any], bound_args: dict[str, Any]) -> list[str]: |
| 141 | + reasons = [] |
| 142 | + symbols = {} |
| 143 | + for name, expected in spec["static_args"].items(): |
| 144 | + actual = bound_args.get(name) |
| 145 | + if actual != expected: |
| 146 | + reasons.append(f"static {name}={actual!r} != {expected!r}") |
| 147 | + for guard in spec["tensor_guards"]: |
| 148 | + tensor = bound_args.get(guard["name"]) |
| 149 | + reason = _tensor_mismatch_reason(tensor, guard, symbols) |
| 150 | + if reason is not None: |
| 151 | + reasons.append(reason) |
| 152 | + return reasons |
| 153 | + |
| 154 | + |
| 155 | +def _tensor_mismatch_reason( |
| 156 | + tensor: Any, |
| 157 | + guard: dict[str, Any], |
| 158 | + symbols: dict[str, int], |
| 159 | +) -> str | None: |
| 160 | + name = guard["name"] |
| 161 | + if not isinstance(tensor, torch.Tensor): |
| 162 | + return f"{name} is {type(tensor).__name__}, expected Tensor" |
| 163 | + expected_dtype = getattr(torch, guard["dtype"]) |
| 164 | + if tensor.dtype is not expected_dtype: |
| 165 | + return f"{name} dtype {tensor.dtype} != torch.{guard['dtype']}" |
| 166 | + if tensor.device.type != guard["device_type"]: |
| 167 | + return f"{name} device type {tensor.device.type} != {guard['device_type']}" |
| 168 | + if tensor.device.index != guard["device_index"]: |
| 169 | + return f"{name} device index {tensor.device.index} != {guard['device_index']}" |
| 170 | + if tensor.dim() != guard["rank"]: |
| 171 | + return f"{name} rank {tensor.dim()} != {guard['rank']}" |
| 172 | + shape_reason = _shape_mismatch_reason(name, tensor, guard, symbols) |
| 173 | + if shape_reason is not None: |
| 174 | + return shape_reason |
| 175 | + if tuple(tensor.stride()) != tuple(guard["stride"]): |
| 176 | + return f"{name} stride {tuple(tensor.stride())} != {tuple(guard['stride'])}" |
| 177 | + return None |
| 178 | + |
| 179 | + |
| 180 | +def _shape_mismatch_reason( |
| 181 | + name: str, |
| 182 | + tensor: torch.Tensor, |
| 183 | + guard: dict[str, Any], |
| 184 | + symbols: dict[str, int], |
| 185 | +) -> str | None: |
| 186 | + for dim, expected in enumerate(guard["shape"]): |
| 187 | + actual = int(tensor.shape[dim]) |
| 188 | + if isinstance(expected, dict): |
| 189 | + symbol = expected["symbol"] |
| 190 | + if expected["min"] is not None and actual < expected["min"]: |
| 191 | + return ( |
| 192 | + f"{name} shape dim {dim}={actual} is less than {symbol} min {expected['min']}" |
| 193 | + ) |
| 194 | + if expected["max"] is not None and actual > expected["max"]: |
| 195 | + return f"{name} shape dim {dim}={actual} is greater than {symbol} max {expected['max']}" |
| 196 | + if symbol in symbols and symbols[symbol] != actual: |
| 197 | + return f"{name} shape dim {dim}={actual} does not equal {symbol}={symbols[symbol]}" |
| 198 | + symbols[symbol] = actual |
| 199 | + elif actual != expected: |
| 200 | + return f"{name} shape dim {dim}={actual} != {expected}" |
| 201 | + return None |
| 202 | + |
| 203 | + |
| 204 | +def _format_no_match( |
| 205 | + bound_args: dict[str, Any], failures: list[tuple[dict[str, Any], list[str]]] |
| 206 | +) -> str: |
| 207 | + lines = ["No export_autograd_triton specialization matched the runtime inputs.", "Received:"] |
| 208 | + for name, value in bound_args.items(): |
| 209 | + if isinstance(value, torch.Tensor): |
| 210 | + lines.append( |
| 211 | + f" {name}: dtype={value.dtype}, device={value.device}, " |
| 212 | + f"shape={tuple(value.shape)}, stride={tuple(value.stride())}" |
| 213 | + ) |
| 214 | + else: |
| 215 | + lines.append(f" {name}: {value!r}") |
| 216 | + lines.append("Expected specializations:") |
| 217 | + for spec, reasons in failures: |
| 218 | + lines.append(f" - {spec['name']}:") |
| 219 | + for reason in reasons: |
| 220 | + lines.append(f" {reason}") |
| 221 | + return "\n".join(lines) |
0 commit comments