-
Notifications
You must be signed in to change notification settings - Fork 122
Expand file tree
/
Copy pathtransforms.py
More file actions
3066 lines (2296 loc) · 101 KB
/
Copy pathtransforms.py
File metadata and controls
3066 lines (2296 loc) · 101 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
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import annotations
from enum import auto, Enum
from itertools import chain
from functools import lru_cache, partial, wraps
import math
from numbers import Number
from typing import Any, TYPE_CHECKING
from collections.abc import Callable
from collections.abc import Sequence
import copy
import inspect
import time
import dataclasses
import thunder.core.utils as utils
from thunder.core import dtypes, prims
from thunder.core.devices import Device
from thunder.core.trace_interpreter import (
interpret_trace as eval_trace,
interpret_trace_to_trace,
trace_interpreter_skip_list,
)
from thunder.core.proxies import (
FloatProxy,
FutureTensorProxy,
NumberProxy,
Proxy,
ProxyTag,
TensorProxy,
variableify,
)
from thunder.core.compile_data import get_compile_data
from thunder.core.langctxs import langctx, Languages
from thunder.core.pytree import tree_flatten, tree_map, tree_unflatten, tree_flatten_with_dataclass
from thunder.core.symbol import BoundSymbol, BoundSymbolInterface, Symbol
from thunder.core.trace import TraceCtx as Trace
from thunder.core.trace import VariableInterface as Variable
from thunder.core.trace import (
set_tracectx,
reset_tracectx,
from_trace,
TraceProvenance,
TraceTag,
)
from thunder.core.utils import (
check,
flatten_func,
safe_map,
safe_map_flat,
const_as,
sequencify,
ProxyDict,
)
import thunder.clang as clang
from thunder.clang import (
full_like,
unsqueeze,
squeeze,
slice_in_dim,
reciprocal,
convolution,
)
from thunder.core.transform_common import (
dce,
Transform,
wrap_return_value_together_with_arguments,
VJPDual,
)
from thunder.core.vjp_utils import make_aug_forward_and_backward
from thunder.extend import Executor
import thunder.torch as ltorch
import torch
# from torch._subclasses.fake_tensor import FakeTensor, FakeTensorMode
import numpy as np
TraceTag.register_tag("AUGMENTED_FORWARD")
ProxyTag.register_tag("RECOMPUTE_IN_BACKWARD")
# TODO This should be a partial of thunder.trace, but that would cause a circular import
# issue today. We should refactor so we dont have a circular import problem.
def construct_trace(inline_trace=False, **extra_kwargs):
import thunder
return thunder.trace(inline_trace=inline_trace, **extra_kwargs)
#
# Functions related to converting lists of bound symbols to and from DAGs, and operations on
# DAGs of bound symbols
#
# TODO Consider adding more dag manipulation functions, currently these functions just support analysis
# and toposorting
# A node in a DAG represents a bsym, with edges defined by the children (outgoing edges) and
# parents (incoming edges) lists
# NOTE Don't compare nodes directly. Compare their bsyms.
class Node:
def __init__(self, bsym: BoundSymbolInterface):
self.bsym = bsym
self.children: list[Node] = []
self.parents: list[Node] = []
self.number: None | int = None
# TODO Consider printing parents and children
def __repr__(self) -> str:
return str(self.bsym)
def __hash__(self) -> int:
utils.check(False, lambda: "Trying to hash a Node. Hash its bsym instead.")
def __eq__(self, other) -> bool:
utils.check(False, lambda: "Trying to compare Nodes for equality. Compare their bsyms' instead.")
# TODO Think about how to model nodes likes comments -- maybe comments should be associated with
# other bound symbols so they are always printed above the bound symbol they refer to?
# Converts a sequence of bound symbols to a directed acyclic graph
# Returns a tuple of
# - a list of all Nodes corresponding to bound symbols without parents
# - a list of all Nodes of bound symbols without children
# Note that nodes without parents or children may be in either list -- running a DCE pass
# before toposorting should remove all nodes without children EXCEPT FOR the return node
def bsym_list_to_dag(
bsyms: Sequence[BoundSymbolInterface], *, producers: None | ProxyDict = None, consumers: None | ProxyDict = None
) -> tuple[list[Node], list[Node]]:
roots: list[Node] = []
leaves: list[Node] = []
return_node: None | Node = None
# Note, we use "line number" as ids for consumers/producers
producers = producers if producers is not None else utils.producers(bsyms, _map_to_numbers=True)
consumers = consumers if consumers is not None else utils.consumers(bsyms, _map_to_numbers=True)
# Constructs a node per bsym, and a bsym id -> node mapping
bsym_id_to_node_map: dict[int, Node] = {}
for bsym_id, bsym in enumerate(bsyms):
node = Node(bsym)
bsym_id_to_node_map[bsym_id] = node
if bsym.sym.id is prims.PrimIDs.RETURN:
utils.check(
return_node is None,
lambda: "Found multiple RETURN nodes while converting a list of bound symbols to a dag",
)
return_node = node
# Adds edges between nodes
for bsym_id, node in bsym_id_to_node_map.items():
has_parents: bool = False
for inp in node.bsym.flat_proxy_args:
producer = producers.get(inp, None)
if producer is None:
continue
producer_node = bsym_id_to_node_map[producer]
parent = bsym_id_to_node_map[producer]
# Checks if the node was already parent to avoid multiple edges between two nodes
already_has_parent: bool = False
for pnode in node.parents:
if producer_node.bsym is pnode.bsym:
already_has_parent = True
break
if not already_has_parent:
node.parents.append(parent)
has_parents = True
if not has_parents:
roots.append(node)
has_children: bool = False
vargs = node.bsym.flat_variableified_proxy_args
for out in node.bsym.flat_proxy_outs:
# Checks that the output is actually produced by this function, and not an input to it
vout = variableify(out)
if vout in vargs:
continue
children = consumers.get(out, [])
for child in children:
has_children = True
child_node = bsym_id_to_node_map[child]
# Checks if the node was already a child to avoid multiple edges between two nodes
already_has_child: bool = False
for cnode in node.children:
if child_node.bsym is cnode.bsym:
already_has_child = True
break
if not already_has_child:
node.children.append(child_node)
if not has_children:
leaves.append(node)
return roots, leaves
class TOPOSORT_ORDER(Enum):
TOP_DOWN = auto()
BOTTOM_UP = auto()
def _default_toposort_selector(eligible_nodes: list[Node]) -> int:
return 0
# Converts a dag of bound symbol nodes into a topologically sorted list of bound symbols
# "selector" must be a function with signature fn(eligible_nodes: list[Node]) -> int, which
# returns the index of the next node to add to the list of bound symbols
# "eligible_nodes" will be a list of all nodes that can appear next in a valid topological
# sorting of the dag (which is dependent on prevoius sorting choices)
# If "toposort_order" is TOP_DOWN then the original nodes should be nodes without parents, and
# eligible nodes will be the set of nodes who have all their parents sorted
# If "toposort_order" is BOTTOM_UP then the original nodes should be a list with just the return node
# (as returned from bsym_list_to_dag()) and eligible nodes will be the set of nodes who have
# all their children sorted
# NOTE Even though the sorting is BOTTOM_UP, the list of bound symbols will be returned in
# a valid (top to bottom) order
def toposort_bsym_dag(
start_nodes: list[Node], toposort_order: TOPOSORT_ORDER, selector: Callable = _default_toposort_selector
) -> list[BoundSymbolInterface]:
sorted: set[BoundSymbolInterface] = set()
bsyms: list[BoundSymbolInterface] = []
eligible_nodes: list[Node] = copy.copy(start_nodes)
while True:
if len(eligible_nodes) == 0:
break
# Picks the next node
idx: int = selector(eligible_nodes)
node: Node = eligible_nodes.pop(idx)
bsyms.append(node.bsym)
sorted.add(node.bsym)
# Identifies additional eligible nodes
# NOTE This doesn't check that the possibly eligible mode wasn't previously eligible or sorted,
# because this is not possible since one of the nodes required for a parent or child to become
# eligible was just sorted.
possibly_eligible = node.parents if toposort_order is TOPOSORT_ORDER.BOTTOM_UP else node.children
for pe_node in possibly_eligible:
required_nodes = pe_node.children if toposort_order is TOPOSORT_ORDER.BOTTOM_UP else pe_node.parents
is_eligible: bool = True
for req in required_nodes:
if req.bsym not in sorted:
is_eligible = False
break
if is_eligible:
eligible_nodes.append(pe_node)
if toposort_order is TOPOSORT_ORDER.BOTTOM_UP:
bsyms.reverse()
return bsyms
#
# Functions related to visitor transforms and modifying traces by tracing new functions
#
# TODO We should consider using alternative datastructures for bound symbols if we're manipulating them inplace.
# Maybe we should be temporarily converting to a deque, or some intermediate datastructure that has to be
# translated into a list.
# Helper function that extends a list with the values in "extension" from the specified starting index "start"
def _insert_extend_list(l: list, start: int, extension: Sequence[Any]) -> None:
for offset, arg in enumerate(extension):
l.insert(start + offset, arg)
# NOTE This operation is inplace. It will modify the trace's bound_symbols.
# NOTE Because this operation is explicitly inplace, it will disregard the trace being "complete".
def insert_inplace(
trc: Trace,
idx: int,
fn: Callable[[], Any],
) -> None:
r"""Calls ``fn`` and record any symbols called into ``trc``, starting at ``idx``.
Args:
trc: Trace to insert :class:`~thunder.core.symbol.BoundSymbol`\s representing ``fn``.
idx: Starting index of ``trc.bound_symbols`` to insert :class:`~thunder.core.symbol.BoundSymbol`\s representing ``fn``.
fn:
.. note::
This operation is inplace. It will modify the given ``trc``'s :attr:`~thunder.core.trace.TraceCtx.bound_symbols`.
.. note::
Because this operation is explicitly inplace, it will disregard whether or not :func:`~thunder.core.trace.TraceCtx.mark_complete` has been called on ``trc`` already.
"""
try:
tracectx_tok = set_tracectx(trc)
trc._complete = False
# Creates a temporary scope to record these operations in
scope = []
trc.push_scope(scope)
fn()
_insert_extend_list(trc.bound_symbols, idx, scope)
finally:
trc.pop_scope()
trc._complete = True
reset_tracectx(tracectx_tok)
# NOTE This operation is inplace. It will modify the trace's bound_symbols.
# NOTE Because this operation is explicitly inplace, it will disregard the trace being "complete".
def replace_inplace(
trc: Trace,
idx: int,
fn: Callable[[BoundSymbol], Any],
) -> None:
r"""Removes ``idx``-th :class:`~thunder.core.symbol.BoundSymbol` of ``trc`` and replace it ``bsyms`` representing ``fn``.
Args:
trc: Trace to insert :class:`~thunder.core.symbol.BoundSymbol`\s representing ``fn``.
idx: Index of :class:`~thunder.core.symbol.BoundSymbol` of ``trc``.
fn: Callable to bake into ``trc``, instead of ``idx``-th :class:`~thunder.core.symbol.BoundSymbol`.
.. note::
This operation is inplace. It will modify the given ``trc``'s :attr:`~thunder.core.trace.TraceCtx.bound_symbols`.
.. note::
Because this operation is explicitly inplace, it will disregard whether or not :func:`~thunder.core.trace.TraceCtx.mark_complete` has been called on ``trc`` already.
"""
try:
tracectx_tok = set_tracectx(trc)
trc._complete = False
# Creates a temporary scope to record these operations in
scope = []
trc.push_scope(scope)
fn(trc.bound_symbols[idx])
del trc.bound_symbols[idx]
_insert_extend_list(trc.bound_symbols, idx, scope)
finally:
trc.pop_scope()
trc._complete = True
reset_tracectx(tracectx_tok)
# Specifies how to preserve or replace bound symbols when visiting them
class VISIT_TYPE(Enum):
INSERT_AFTER = auto()
INSERT_BEFORE = auto()
REPLACE = auto()
NO_OP = auto()
# Creates a new trace from "trace_from" by calling "visit" on its bound symbols ("bsyms").
# visit(bsym: BoundSymbolInterface) -> VISIT_TYPE should call operations
# as if executing a program, and those operations will be recorded into the
# new trace.
# If visit() returns INSERT_AFTER for a bsym then that bsym will be copied
# to the new trace before visit() is called. This is useful when augmenting the bound
# symbols in an existing trace.
# If visit() returns INSERT_BEFORE for a bsym then that bsym will be copied to the new trace
# after visit() is called. This is also useful when augmenting the bound symbols in an existing
# trace.
# If visit() returns REPLACE for a bsym then that bsym will not be copied to the new trace.
# TODO Suggest a mechanism to preserve the original bound symbol with operations
# recorded both before and after it. This could be done by passing the (sub)scope to visit() for
# direct modification, acquiring the trace's current scope through the trace ctx and modifying it
# directly (this can be done today), or adding a record() function that is a sugar for the previous
# approach. Perhaps both passing the scope directly to visit() and adding record() would be helpful.
# TODO(crcrpar): Think about providing a guide how to let thunder "claim" if this is called after
# `thunder.executors.transform_for_execution`.
def visitor_transform(trace_from: Trace, visit: Callable, *, provenance: None | str = None) -> Trace:
trc: Trace = from_trace(trace_from)
try:
tracectx_tok = set_tracectx(trc)
for bsym in trace_from.bound_symbols:
try:
# Creates a temporary scope to support copying the original bsym BEFORE
# the operations performed by visit(), even though this doesn't know whether to
# copy the original bsym until after visit() completes
scope = []
trc.push_scope(scope)
visit_type = visit(bsym)
if visit_type is VISIT_TYPE.INSERT_AFTER:
trc.bound_symbols.append(bsym)
if visit_type is not VISIT_TYPE.NO_OP:
trc.bound_symbols.extend(scope)
else:
trc.bound_symbols.append(bsym)
if visit_type is VISIT_TYPE.INSERT_BEFORE:
trc.bound_symbols.append(bsym)
finally:
# Restores the trc's scope
trc.pop_scope()
if provenance is not None:
trc.set_provenance(TraceProvenance(provenance))
return trc
finally:
reset_tracectx(tracectx_tok)
#
# Composable transforms
#
# Helper function to add a transform
def add_transform(
cfn: Callable,
*,
transform: Transform | list[Transform],
disable_torch_autograd_support=False,
) -> Callable:
from thunder.common import CompileData
cd: None | Any = getattr(cfn, "_lc_cd", None)
utils.check(cd is not None, lambda: "Can only transform compiled thunder functions")
utils.check(isinstance(cd, CompileData), lambda: f"Found an unknown compile data attribute {cd}")
if isinstance(transform, Transform):
transform = [transform]
else:
utils.check(
all(isinstance(t, Transform) for t in transform),
lambda: "transform must be an instance of Transform or a list of Transform instances.",
)
assert cd.using_jit
from thunder import jit
# todo: move _lc_transforms to compile_data
transforms = cfn._lc_transforms + transform
jfn = jit(
cd.fn,
langctx=cd.langctx,
executors=cd.executors_list,
sharp_edges=cd.sharp_edges,
# cache, interpretation?
transforms=transforms,
debug_options=cd.debug_options,
disable_torch_autograd=cd.disable_torch_autograd_support or disable_torch_autograd_support,
**cd.compile_options,
)
return jfn
# The no-op transform. A trivial composable transform, only useful as an example.
class _NoopTransform(Transform):
def transform_trace_pre_prologue(
self, prologue_trace: Trace, computation_trace: Trace, epilogue_trace: Trace | None, **kwargs
) -> Trace:
start_time_ns = time.perf_counter_ns()
noop_trace = from_trace(computation_trace)
tracectx_tok: Any
try:
tracectx_tok = set_tracectx(noop_trace)
prims.comment("This comment added by the no-op transform")
finally:
reset_tracectx(tracectx_tok)
noop_trace.bound_symbols.extend(computation_trace.bound_symbols)
end_time_ns = time.perf_counter_ns()
elapsed_time_ns = end_time_ns - start_time_ns
elapsed_time_millis = elapsed_time_ns // 1000000
noop_trace.set_provenance(TraceProvenance(f"No-op Transform (took {elapsed_time_millis} milliseconds)"))
return prologue_trace, noop_trace, computation_trace
def noop(cfn: Callable) -> Callable:
_noop_transform = _NoopTransform()
return add_transform(cfn, transform=_noop_transform)
# The comment fusions transform. Just adds a comment before and after each fusion.
# This is an example of a post-optimization transform.
class _CommentFusionsTransform(Transform):
def transform_trace_post_optimization(self, trace: Trace, **kwargs) -> Trace:
start_time_ns = time.perf_counter_ns()
commented_trace = from_trace(trace)
nbsyms: list[BoundSymbol] = []
for bsym in trace.bound_symbols:
if bsym.sym.is_fusion:
fusion_name = bsym.sym.name
pre_comment_bsym = prims.comment.bind(f"Before {fusion_name}", output=None)
post_comment_bsym = prims.comment.bind(f"After {fusion_name}", output=None)
nbsyms.extend([pre_comment_bsym, bsym, post_comment_bsym])
else:
nbsyms.append(bsym)
commented_trace.bound_symbols = nbsyms
end_time_ns = time.perf_counter_ns()
elapsed_time_ns = end_time_ns - start_time_ns
elapsed_time_millis = elapsed_time_ns // 1000000
commented_trace.set_provenance(TraceProvenance(f"Comment Fusions (took {elapsed_time_millis} milliseconds)"))
return commented_trace
def comment_fusions(cfn: Callable) -> Callable:
return add_transform(cfn, _CommentFusionsTransform)
#
# Helper functions for composable transforms
#
# Flattens a list of bound symbols, returning the flattened list
def flatten_for_transform(should_flatten: Callable, bsyms: list[BoundSymbol]) -> list[BoundSymbol]:
flattened: list[BoundSymbol] = []
def _flatten(bsym: BoundSymbol):
if should_flatten(bsym):
check(
len(bsym.subsymbols) > 0,
lambda: f"No grad rule found for {bsym} and no subsymbols inside it to create a grad formula",
)
for sbsym in bsym.subsymbols:
_flatten(sbsym)
else:
flattened.append(bsym)
for bsym in bsyms:
_flatten(bsym)
return flattened
#
# Phantom grad transform
#
#
# Functions related to functionalizing ThunderOptimizedModules
#
# TODO Test with buffers
def populate_grads(grads: list[TensorProxy], tom: None | torch.nn.Module = None, args=None, kwargs=None) -> None:
idx: int = 0
from thunder import ThunderModule, compile_data
if isinstance(tom, ThunderModule) or compile_data(tom).using_jit:
assert args is not None, "populate grad needs args (and possibly kwargs) to work with ThunderModules"
if kwargs is None:
kwargs = {}
_, computation_inputs, _ = compile_data(tom).get_computation_and_inputs(*args, **kwargs)
for p in computation_inputs:
if isinstance(p, torch.Tensor) and p.requires_grad:
# Supports grad accumulation (like when weight tying)
if p.grad is not None:
p.grad += grads[idx]
else:
p.grad = grads[idx]
idx += 1
return
# Short-circuits if there are no args or kwargs
if args is None and kwargs is None:
return
flats, _ = tree_flatten((args, kwargs))
for f in flats:
if isinstance(f, torch.Tensor) and f.requires_grad:
f.grad = grads[idx]
idx += 1
def extract_grads(module: torch.nn.Module) -> tuple[torch.Tensor, ...]:
grads = tuple(
f.grad
for f in chain(module.parameters(), module.buffers())
if isinstance(f, torch.Tensor) and f.requires_grad and f.grad is not None
)
return grads
def clear_grads(module: torch.nn.Module) -> None:
if not isinstance(module, torch.nn.Module):
return
for p in module.parameters():
p.grad = None
for b in module.buffers():
b.grad = None
_grad_fn_map: dict[Any, Callable] = {}
def register_grad(sym_or_id: Symbol | Any, gradfn: Callable) -> None:
id: Any = sym_or_id
if isinstance(sym_or_id, Symbol):
id = sym_or_id.id
# The gradfn are expected to be written in terms of torch functions by
# default even if the original forward function could be written in terms of
# other languages. We don't want to have developers worry about the language
# context when writing grad functions. If the grad function is written in
# terms of another language, developers can always wrap the gradfn in an
# appropriate language context that will take precedence over the default
# torch language context.
_grad_fn_map[id] = langctx(Languages.TORCH)(gradfn)
# Grad functions for prims
from thunder.core.prims import PrimIDs as pids, get_grad, put_grad
# A generalization of prims.put_grad to pytrees
# TODO Consider validating that the specs are the same
# TODO Consider validating that that object requires grad (and filtering o.w.)
def put_grads(a, g):
flats, _ = tree_flatten(a)
flatgrads, _ = tree_flatten(g)
for f, fg in zip(flats, flatgrads):
if isinstance(f, TensorProxy) and isinstance(fg, TensorProxy):
put_grad(f, fg)
#
# Unpacking operator grads
#
# NOTE prims.unpack_empty_dict creates no grad associations
register_grad(pids.UNPACK_EMPTY_DICT, prims.unpack_empty_dict)
# NOTE prims.unpack_key creates no grad associations
register_grad(pids.UNPACK_KEY, prims.unpack_key)
# NOTE prims.unpack_sequence creates no grad associations
register_grad(pids.UNPACK_SEQUENCE, prims.unpack_sequence)
#
# Data movement and transformation operator grads
#
def _convert_element_type_prim_grad(a: Number | TensorProxy, dtype: type | dtypes.dtype) -> Number | TensorProxy:
fwd = prims.convert_element_type(a, dtype)
g = get_grad(fwd)
g_converted = prims.convert_element_type(g, dtypes.to_dtype(a))
put_grad(a, g_converted)
return fwd
register_grad(pids.CONVERT_ELEMENT_TYPE, _convert_element_type_prim_grad)
#
# Tensor creation operator grads
#
# NOTE prims.full creates no grad associations
register_grad(pids.FULL, prims.full)
# NOTE prims.iota creates no grad associations
register_grad(pids.IOTA, prims.iota)
def _uniform_grad(shape, minval, maxval, *, device, dtype):
fwd, saved = uniform_aug_fwd(shape, minval, maxval, device=device, dtype=dtype)
g = get_grad(fwd)
_, gminval, gmaxval = uniform_backward(*saved, g)
put_grads((minval, maxval), (gminval, gmaxval))
return fwd
register_grad(pids.UNIFORM, _uniform_grad)
#
# Reshaping and permuting operator grads
#
def _broadcast_in_dim_prim_grad(
a: TensorProxy, shape: Sequence[int], broadcast_dimensions: Sequence[int]
) -> TensorProxy:
fwd = prims.broadcast_in_dim(a, shape, broadcast_dimensions)
g = get_grad(fwd)
unit_dims = tuple(i for i, s in enumerate(a.shape) if s == 1)
bcast_dims = tuple(b for i, b in enumerate(broadcast_dimensions) if i not in unit_dims)
reduce_dims = tuple(s for i, s in enumerate(range(len(shape))) if i not in bcast_dims)
# NOTE When the reduce_dims tuple is empty, pytorch reduces all dimensions.
# In this case, we do not want to reduce any dimensions, so skip this sum.
if len(reduce_dims) > 0:
g = ltorch.sum(g, reduce_dims)
# NOTE This must be clang.unsqueeze because torch.unsqueeze, unlike clang.unsqueeze, only accepts an integer
# (put another way, torch only allows one unsqueeze at a time)
g = clang.unsqueeze(g, unit_dims)
put_grad(a, g)
return fwd
register_grad(pids.BROADCAST_IN_DIM, _broadcast_in_dim_prim_grad)
def _cat_prim_grad(tensors: list[TensorProxy], /, dim: int) -> TensorProxy:
fwd = prims.cat(tensors, dim)
g = get_grad(fwd)
slice_start: int = 0
t: TensorProxy
for t in tensors:
dim_len: int = t.shape[dim]
slice_end: int = slice_start + dim_len
g_slice: TensorProxy = clang.slice_in_dim(g, slice_start, slice_end, dim=dim)
slice_start = slice_end
put_grad(t, g_slice)
return fwd
register_grad(pids.CAT, _cat_prim_grad)
def _shallow_copy_prim_grad(a: TensorProxy) -> TensorProxy:
fwd = prims.shallow_copy(a)
g = get_grad(fwd)
put_grad(a, g)
return fwd
register_grad(pids.SHALLOW_COPY, _shallow_copy_prim_grad)
def _update_aliases_prim_grad(tensors: tuple[TensorProxy, ...]) -> tuple[TensorProxy, ...]:
fwd_tensors = prims.update_aliases(tensors)
for fwd_t, t in zip(fwd_tensors, tensors):
g = get_grad(fwd_t)
put_grad(t, g)
return fwd_tensors
register_grad(pids.UPDATE_ALIASES, _update_aliases_prim_grad)
def _reshape_prim_grad(a: TensorProxy, shape: tuple[int, ...]) -> TensorProxy:
fwd = prims.reshape(a, shape)
g = get_grad(fwd)
a_grad = prims.reshape(g, a.shape)
put_grad(a, a_grad)
return fwd
register_grad(pids.RESHAPE, _reshape_prim_grad)
def _slice_prim_grad(
a: TensorProxy, start_indices: Sequence[int], end_indices: Sequence[int], strides: None | Sequence[int] = None
) -> TensorProxy:
fwd = prims.slice_prim(a, start_indices, end_indices, strides)
g = get_grad(fwd)
padding = None
if strides is None or np.all(np.equal(strides, 1)):
padding = tuple(zip(start_indices, np.subtract(a.shape, end_indices), (0,) * len(start_indices)))
else:
real_limits = np.add(
start_indices,
np.where(np.equal(g.shape, 0), 0, np.add(1, np.multiply(np.subtract(g.shape, 1), strides))),
)
padding = tuple(zip(start_indices, np.subtract(a.shape, real_limits), np.subtract(strides, 1)))
# Converts NumPy numbers to Python ints
# TODO Should we support NumPy numbers better?
padding = tree_map(int, padding)
a_grad = prims.pad(g, const_as(0, g.dtype), padding)
put_grad(a, a_grad)
return fwd
register_grad(pids.SLICE, _slice_prim_grad)
def _squeeze_prim_grad(a: TensorProxy, /, dims: tuple[int, ...]) -> TensorProxy:
fwd = prims.squeeze(a, tuple(dims))
g = get_grad(fwd)
# NOTE This calls clang.unsqueeze, and not torch.unsqueeze, because torch.unsqueeze only supports
# unsqueezing a single dimension
a_grad = clang.unsqueeze(g, dims)
put_grad(a, a_grad)
return fwd
register_grad(pids.SQUEEZE, _squeeze_prim_grad)
def _take_prim_grad(a: TensorProxy, index: TensorProxy, dim: int) -> TensorProxy:
fwd = prims.take(a, index, dim)
g = get_grad(fwd)
# TODO Switch to ltorch.index_add?
# NOTE Intentionally not calling zeros_like to avoid preserving a
# TODO Update to call ltorch.zeros
zeros = prims.full(a.shape, fill_value=0, device=a.device, dtype=a.dtype)
a_grad = prims.index_add(zeros, index, g, dim)
put_grad(a, a_grad)
return fwd
register_grad(pids.TAKE, _take_prim_grad)
def _gather_prim_grad(a: TensorProxy, index: TensorProxy, dim: int) -> TensorProxy:
fwd = prims.gather(a, index, dim)
g = get_grad(fwd)
# NOTE Intentionally not calling zeros_like to avoid preserving TensorProxy a.
# TODO Update to call ltorch.zeros
zeros = prims.full(a.shape, fill_value=0, device=a.device, dtype=a.dtype)
a_grad = prims.scatter_add(zeros, index, g, dim)
put_grad(a, a_grad)
return fwd
register_grad(pids.GATHER, _gather_prim_grad)
def _scatter_prim_grad(a: TensorProxy, /, index: TensorProxy, src: TensorProxy | Number, dim: int) -> TensorProxy:
fwd = prims.scatter(a, index, src, dim)
grad = get_grad(fwd)
a_grad = prims.scatter(grad, index, 0, dim)
put_grad(a, a_grad)
if isinstance(src, TensorProxy):
# NOTE: this is exactly what PyTorch is doing.
# As such, it has the very same limitations. I.e.
# the grad is not going to be correct unless the index list
# (..., index[...], ...) does not have repeated elements
src_grad = prims.gather(grad, index, dim)
put_grad(src, src_grad)
return fwd
register_grad(pids.SCATTER, _scatter_prim_grad)
def _index_copy_grad(a: TensorProxy, /, index: TensorProxy, src: TensorProxy, dim: int) -> TensorProxy:
fwd = prims.index_copy(a, index, src, dim)
grad = get_grad(fwd)
# a_grad = grad.index_fill(dim, index, 0)
# Unfortunately, we do not have `index_fill` for now
# TODO: replace with `index_fill`
grad_dim = utils.canonicalize_dim(grad.ndim, dim)
index_len = len(index)
index_unsqueeze_shape = [1] * grad.ndim
index_unsqueeze_shape[grad_dim] = index_len
index_expand_shape = list(grad.shape)
index_expand_shape[grad_dim] = index_len
a_grad = prims.scatter(grad, index.reshape(index_unsqueeze_shape).expand(*index_expand_shape), 0, dim)
put_grad(a, a_grad)
if src.ndim > 0:
src_grad = prims.take(grad, index, dim).expand_as(src)
else:
src_grad = prims.take(grad, index.squeeze(0))
put_grad(src, src_grad)
return fwd
register_grad(pids.INDEX_COPY, _index_copy_grad)
def _scatter_add_prim_grad(a: TensorProxy, /, index: TensorProxy, value: TensorProxy, dim: int) -> TensorProxy:
utils.check(
not value._requires_grad or value.shape == index.shape,
lambda: "The gradient for the value Tensor is implemented only when value.shape == index.shape. "
"value shape is {value.shape} while index shape is {index.shape}",
)
fwd = prims.scatter_add(a, index, value, dim)
g = get_grad(fwd)
# NOTE The value gradient is only correct when src.shape == index.shape.
# See https://github.com/pytorch/pytorch/issues/27614#issuecomment-564648819
value_grad = prims.gather(g, index, dim)
put_grads((a, value), (g, value_grad))
return fwd
register_grad(pids.SCATTER_ADD, _scatter_add_prim_grad)
def _take_along_axis_prim_grad(a: TensorProxy, index: TensorProxy, dim: int) -> TensorProxy:
fwd = prims.take_along_axis(a, index, dim)
g = get_grad(fwd)
# NOTE Intentionally not calling zeros_like to avoid preserving TensorProxy a.
# TODO Update to call ltorch.zeros
zeros = prims.full(a.shape, fill_value=0, device=a.device, dtype=a.dtype)
a_grad = prims.scatter_add(zeros, index, g, dim)
put_grad(a, a_grad)
return fwd
register_grad(pids.TAKE_ALONG_AXIS, _take_along_axis_prim_grad)
def _transpose_prim_grad(a: TensorProxy, permutation: tuple[int, ...]) -> TensorProxy:
fwd = prims.transpose(a, tuple(permutation))
g = get_grad(fwd)
undo = _argsort(permutation)
a_grad = prims.transpose(g, tuple(undo))
put_grad(a, a_grad)
return fwd
register_grad(pids.TRANSPOSE, _transpose_prim_grad)
#
# Memory layout operator grads
#
def _stride_order_prim_grad(a: TensorProxy, /, order: Sequence[int]) -> TensorProxy:
fwd = prims.stride_order(a, order)
g = get_grad(fwd)
put_grad(a, g)
return fwd
register_grad(pids.STRIDE_ORDER, _stride_order_prim_grad)
#
# Elementwise unary operator grads
#
def _abs_prim_grad(a: Number | TensorProxy) -> Number | TensorProxy:
fwd = prims.abs(a)
g = get_grad(fwd)
put_grad(a, g * ltorch.sign(a))
return fwd
register_grad(pids.ABS, _abs_prim_grad)
def _cos_prim_grad(a: Number | TensorProxy) -> Number | TensorProxy:
fwd = prims.cos(a)