|
| 1 | +# Copyright (c) Meta Platforms, Inc. and affiliates. |
| 2 | +# All rights reserved. |
| 3 | +# |
| 4 | +# This source code is licensed under the BSD-style license found in the |
| 5 | +# LICENSE file in the root directory of this source tree. |
| 6 | + |
| 7 | +import copy |
| 8 | +import operator |
| 9 | +from dataclasses import dataclass |
| 10 | + |
| 11 | +import torch.fx as fx |
| 12 | +from torch._functorch.partitioners import ( |
| 13 | + _extract_fwd_bwd_outputs, |
| 14 | + _extract_graph_with_inputs_outputs, |
| 15 | + is_sym_node, |
| 16 | +) |
| 17 | +from torch.fx._lazy_graph_module import _make_graph_module |
| 18 | + |
| 19 | +from torchtitan.experiments.graph_trainer.graph_pp.utils import ( |
| 20 | + is_fake_tensor_node, |
| 21 | + output_names, |
| 22 | + placeholder_names, |
| 23 | + rename_placeholder, |
| 24 | + trace_graph_pp_graph, |
| 25 | + unique_in_order, |
| 26 | +) |
| 27 | + |
| 28 | + |
| 29 | +@dataclass(frozen=True, slots=True) |
| 30 | +class GraphPPDiDwSplit: |
| 31 | + """Backward graph split into dI and dW GraphPP callables. |
| 32 | +
|
| 33 | + Attributes: |
| 34 | + bw_di_module (fx.GraphModule): Backward-input graph. It computes input |
| 35 | + gradients to send to the previous PP stage and live-ins needed by |
| 36 | + the backward-weight graph. |
| 37 | + bw_dw_module (fx.GraphModule): Backward-weight graph. It consumes the |
| 38 | + dW live-ins and computes parameter gradients. |
| 39 | + num_input_grads (int): Number of leading ``bw_di_module`` outputs that |
| 40 | + are input_grads_to_prev. |
| 41 | + bw_di_input_names (tuple[str, ...]): ``bw_di_module`` placeholder |
| 42 | + names. |
| 43 | + bw_di_output_names (tuple[str, ...]): ``bw_di_module`` output names. |
| 44 | + bw_dw_input_names (tuple[str, ...]): ``bw_dw_module`` placeholder |
| 45 | + names. |
| 46 | + bw_dw_output_names (tuple[str, ...]): ``bw_dw_module`` output names. |
| 47 | + """ |
| 48 | + |
| 49 | + bw_di_module: fx.GraphModule |
| 50 | + bw_dw_module: fx.GraphModule |
| 51 | + num_input_grads: int |
| 52 | + bw_di_input_names: tuple[str, ...] |
| 53 | + bw_di_output_names: tuple[str, ...] |
| 54 | + bw_dw_input_names: tuple[str, ...] |
| 55 | + bw_dw_output_names: tuple[str, ...] |
| 56 | + |
| 57 | + |
| 58 | +def _reorder_backward_outputs_for_di( |
| 59 | + gm: fx.GraphModule, *, num_param_grads: int |
| 60 | +) -> int: |
| 61 | + outputs = gm.graph.find_nodes(op="output") |
| 62 | + if len(outputs) != 1: |
| 63 | + raise ValueError(f"Expected exactly one output node, found {len(outputs)}") |
| 64 | + output = outputs[0] |
| 65 | + if not isinstance(output.args[0], tuple): |
| 66 | + raise ValueError("Backward graph output must be a tuple") |
| 67 | + |
| 68 | + output_values = output.args[0] |
| 69 | + if len(output_values) < num_param_grads: |
| 70 | + raise ValueError( |
| 71 | + f"Backward graph has {len(output_values)} outputs but " |
| 72 | + f"{num_param_grads} parameter grads were requested" |
| 73 | + ) |
| 74 | + |
| 75 | + # The upstream extractor treats the first outputs as the "forward" side. |
| 76 | + # For dI/dW splitting, those early outputs are the input grads that PP |
| 77 | + # schedules send to the previous stage before parameter grads are computed. |
| 78 | + param_grads = output_values[:num_param_grads] |
| 79 | + input_grads = output_values[num_param_grads:] |
| 80 | + with gm.graph.inserting_after(output): |
| 81 | + new_output = gm.graph.output(tuple(input_grads + param_grads)) |
| 82 | + new_output.meta.update(output.meta) |
| 83 | + gm.graph.erase_node(output) |
| 84 | + gm.graph.lint() |
| 85 | + gm.recompile() |
| 86 | + return len(input_grads) |
| 87 | + |
| 88 | + |
| 89 | +def _collect_saved_values_for_dw( |
| 90 | + bw_gm: fx.GraphModule, |
| 91 | + di_graph: fx.Graph, |
| 92 | + dw_output_nodes: list[fx.Node] | None = None, |
| 93 | +) -> tuple[list[fx.Node], list[fx.Node]]: |
| 94 | + di_node_names = {node.name for node in di_graph.nodes if node.op != "output"} |
| 95 | + dw_output_set = set(dw_output_nodes or ()) |
| 96 | + saved_values: list[fx.Node] = [] |
| 97 | + saved_sym_nodes: list[fx.Node] = [] |
| 98 | + |
| 99 | + for node in bw_gm.graph.nodes: |
| 100 | + if node.name not in di_node_names: |
| 101 | + continue |
| 102 | + dw_users = [ |
| 103 | + user |
| 104 | + for user in node.users |
| 105 | + if user.name not in di_node_names and user.op != "output" |
| 106 | + ] |
| 107 | + is_dw_output = node in dw_output_set |
| 108 | + if not dw_users and not is_dw_output: |
| 109 | + continue |
| 110 | + if node.op == "get_attr": |
| 111 | + # get_attr nodes are graph constants, such as FlexAttention's |
| 112 | + # mask/score submodules. The dW graph should retain them as |
| 113 | + # get_attr references instead of receiving Python objects as |
| 114 | + # runtime live-ins from the dI graph. |
| 115 | + continue |
| 116 | + if is_sym_node(node): |
| 117 | + # Symbolic shape values used by dW must stay as SymInt live-ins, |
| 118 | + # not tensor placeholders. |
| 119 | + saved_sym_nodes.append(node) |
| 120 | + elif ( |
| 121 | + "tensor_meta" not in node.meta |
| 122 | + and node.op == "call_function" |
| 123 | + and not is_fake_tensor_node(node) |
| 124 | + ): |
| 125 | + # Non-tensor tuple-like results flow through getitem leaves. Save |
| 126 | + # the leaves because the dW graph consumes those concrete values. |
| 127 | + users = list(node.users) |
| 128 | + if not all(user.target == operator.getitem for user in users): |
| 129 | + raise ValueError( |
| 130 | + f"Non-tensor multi-output node {node.name} has unexpected users" |
| 131 | + ) |
| 132 | + saved_values.extend(user for user in users if user in dw_users) |
| 133 | + else: |
| 134 | + if ( |
| 135 | + not is_dw_output |
| 136 | + and "tensor_meta" in node.meta |
| 137 | + and all(is_sym_node(user) for user in dw_users) |
| 138 | + ): |
| 139 | + # If dW only needs shape reads from this tensor, pass the |
| 140 | + # symbolic reads instead of keeping the tensor live. |
| 141 | + saved_sym_nodes.extend(dw_users) |
| 142 | + else: |
| 143 | + saved_values.append(node) |
| 144 | + |
| 145 | + return ( |
| 146 | + unique_in_order(saved_values), |
| 147 | + unique_in_order(saved_sym_nodes), |
| 148 | + ) |
| 149 | + |
| 150 | + |
| 151 | +def split_di_dw_graph( |
| 152 | + bw_module: fx.GraphModule, |
| 153 | + *, |
| 154 | + num_param_grads: int, |
| 155 | +) -> GraphPPDiDwSplit | None: |
| 156 | + """Split a backward graph into input-gradient and weight-gradient graphs. |
| 157 | +
|
| 158 | + Contract: |
| 159 | + Input: |
| 160 | + backward(saved_values_for_backward, output_grads_from_next?) |
| 161 | + -> param_grads, input_grads_to_prev |
| 162 | +
|
| 163 | + Output: |
| 164 | + bw_di(saved_values_for_backward, output_grads_from_next?) |
| 165 | + -> input_grads_to_prev, dw_live_ins |
| 166 | +
|
| 167 | + bw_dw(dw_live_ins) |
| 168 | + -> param_grads |
| 169 | +
|
| 170 | + Terms: |
| 171 | + ``num_param_grads`` is the leading output count for parameter gradients. |
| 172 | + ``input_grads_to_prev`` are remaining backward outputs sent to the |
| 173 | + previous PP stage. ``dw_live_ins`` are values computed by bw_di that the |
| 174 | + dW graph still needs. ``saved_sym_nodes`` carries symbolic shape live-ins. |
| 175 | +
|
| 176 | + If a stage has no input grads, GraphPP skips ``BACKWARD_INPUT`` and keeps |
| 177 | + the original full backward graph for ``BACKWARD_WEIGHT``. This pass runs |
| 178 | + after AC/remat has already materialized recomputed backward nodes, so those |
| 179 | + nodes are treated like ordinary backward graph nodes. |
| 180 | +
|
| 181 | + Args: |
| 182 | + bw_module (fx.GraphModule): Backward graph whose outputs are ordered as |
| 183 | + parameter gradients followed by input gradients. |
| 184 | + num_param_grads (int): Number of leading backward outputs that are |
| 185 | + parameter-gradient slots. |
| 186 | +
|
| 187 | + Returns: |
| 188 | + GraphPPDiDwSplit | None: Split dI/dW graph modules and metadata, or |
| 189 | + ``None`` when the stage has no input gradients and the full backward |
| 190 | + graph should be used unchanged. |
| 191 | +
|
| 192 | + Raises: |
| 193 | + ValueError: If ``num_param_grads`` is invalid or the backward graph |
| 194 | + does not match the expected flat output convention. |
| 195 | + """ |
| 196 | + if num_param_grads < 0: |
| 197 | + raise ValueError(f"num_param_grads must be non-negative, got {num_param_grads}") |
| 198 | + |
| 199 | + bw_gm = copy.deepcopy(bw_module) |
| 200 | + for placeholder in list(bw_gm.graph.find_nodes(op="placeholder")): |
| 201 | + if placeholder.name.startswith("tangent"): |
| 202 | + rename_placeholder( |
| 203 | + bw_gm, |
| 204 | + placeholder, |
| 205 | + f"graph_pp_runtime_grad{placeholder.name[len('tangent') :]}", |
| 206 | + ) |
| 207 | + |
| 208 | + num_input_grads = _reorder_backward_outputs_for_di( |
| 209 | + bw_gm, num_param_grads=num_param_grads |
| 210 | + ) |
| 211 | + trace_graph_pp_graph("graph_pp_split_di_dw_input", bw_gm) |
| 212 | + if num_input_grads == 0: |
| 213 | + return None |
| 214 | + |
| 215 | + placeholders = list(bw_gm.graph.find_nodes(op="placeholder")) |
| 216 | + di_outputs, dw_outputs, di_output_descs, dw_output_descs = _extract_fwd_bwd_outputs( |
| 217 | + bw_gm, num_fwd_outputs=num_input_grads |
| 218 | + ) |
| 219 | + di_closure_graph = _extract_graph_with_inputs_outputs( |
| 220 | + bw_gm.graph, |
| 221 | + placeholders, |
| 222 | + di_outputs, |
| 223 | + di_output_descs, |
| 224 | + "forward", |
| 225 | + ignore_must_be_in_fw_bw=True, |
| 226 | + ) |
| 227 | + saved_values, saved_sym_nodes = _collect_saved_values_for_dw( |
| 228 | + bw_gm, |
| 229 | + di_closure_graph, |
| 230 | + dw_outputs, |
| 231 | + ) |
| 232 | + di_runtime_outputs = [*di_outputs, *saved_values, *saved_sym_nodes] |
| 233 | + di_runtime_output_descs = list(di_output_descs) + [None] * ( |
| 234 | + len(saved_values) + len(saved_sym_nodes) |
| 235 | + ) |
| 236 | + bw_di_graph = _extract_graph_with_inputs_outputs( |
| 237 | + bw_gm.graph, |
| 238 | + placeholders, |
| 239 | + di_runtime_outputs, |
| 240 | + di_runtime_output_descs, |
| 241 | + "bw_di", |
| 242 | + ignore_must_be_in_fw_bw=True, |
| 243 | + ) |
| 244 | + bw_dw_graph = _extract_graph_with_inputs_outputs( |
| 245 | + bw_gm.graph, |
| 246 | + [*saved_values, *saved_sym_nodes], |
| 247 | + dw_outputs, |
| 248 | + dw_output_descs, |
| 249 | + "bw_dw", |
| 250 | + ignore_must_be_in_fw_bw=True, |
| 251 | + ) |
| 252 | + bw_di_module = _make_graph_module(bw_gm, bw_di_graph) |
| 253 | + bw_dw_module = _make_graph_module(bw_gm, bw_dw_graph) |
| 254 | + bw_di_module.graph.lint() |
| 255 | + bw_dw_module.graph.lint() |
| 256 | + bw_di_module.recompile() |
| 257 | + bw_dw_module.recompile() |
| 258 | + trace_graph_pp_graph("graph_pp_bw_di", bw_di_module) |
| 259 | + trace_graph_pp_graph("graph_pp_bw_dw", bw_dw_module) |
| 260 | + |
| 261 | + return GraphPPDiDwSplit( |
| 262 | + bw_di_module=bw_di_module, |
| 263 | + bw_dw_module=bw_dw_module, |
| 264 | + num_input_grads=num_input_grads, |
| 265 | + bw_di_input_names=placeholder_names(bw_di_module), |
| 266 | + bw_di_output_names=output_names(bw_di_module), |
| 267 | + bw_dw_input_names=placeholder_names(bw_dw_module), |
| 268 | + bw_dw_output_names=output_names(bw_dw_module), |
| 269 | + ) |
0 commit comments