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
// SPDX-License-Identifier: MIT OR Apache-2.0
//! SoftmaxLastDimBackward — fused backward helper for the
//! `SoftmaxLastDim` forward. Phase 7.6 step 4 (continued — first
//! backward helper migrated; activates the registry's
//! `BackwardKind::Fused(id)` connection for the first time).
//!
//! Provides:
//! - [`entry`] — the metadata-side `FusedOpEntry`.
//! - `recipe` — the op's closed-form backward as portable, shape-/rank-
//! polymorphic `PatternNode` data (Increment C slice 1, T8).
//! - [`decompose`] — re-emits `recipe` through the
//! [`crate::registry::decompose_via_recipe`] bridge.
//!
//! The backward formula is `s * (g - sum(g * s, last_dim,
//! keepdim=true))` where `s` is the forward output and `g` is the
//! upstream gradient. Two inputs `[y, upstream]`; parameterless.
//!
//! ## Architectural note — registry purpose for backward helpers
//!
//! Backward helper entries serve a different role from forward
//! entries: there is no user-decomposed form to recognize (the
//! matcher is always stubbed). The `decompose` IS a "synthesize from
//! primitives" recipe, though: the closed-form backward expression
//! is a real 6-node primitive subgraph (the same `SumDim` + keepdim +
//! `BroadcastTo` idiom the forward `softmax_last_dim` recipe uses),
//! so per G2 it decomposes totally — never a basis-gap self-return.
//! T8 (Increment C slice 1) migrated that subgraph from an imperative
//! builder to the portable `recipe` datum, and in doing so exercised
//! the registry's `BackwardKind::Fused(id)` edge end-to-end on a data
//! recipe for the first time. Beyond the recipe, the registry entry
//! also exists to:
//!
//! - declare the op's identity (FusedOpId, FusedOpParams variant,
//! shape/dtype rules);
//! - serve as the target of `BackwardKind::Fused(id)` from the
//! matching forward entry (the architectural connection this
//! commit activates);
//! - host per-backend `BackendImpl` registrations in the
//! `FusedKernelRegistry` (step 6 extension; pending — the
//! binding-table already covers CPU dispatch via the legacy
//! `OpKind` route).
//!
//! Higher-order gradients (backward through this helper itself)
//! panic per `NodeHandle::backward`'s MVP behavior. The registry entry's
//! `backward` field reflects this: [`BackwardKind::NotDifferentiable`].
use crate;
use crate::;
use ;
use ShapeExpr;
use ;
use OnceLock;
/// Metadata-side registry entry for SoftmaxLastDimBackward.
/// Shape rule: output equals input 0 (the forward softmax output).
/// Dtype rule: output dtype equals input 0 (the forward softmax output).
/// SoftmaxLastDimBackward's primitive recipe as **portable data** (Increment C
/// slice 1, T8 — the first *backward* helper migrated, and the op that
/// activates the registry's `BackwardKind::Fused(SOFTMAX_LAST_DIM_BACKWARD)`
/// edge end-to-end on a data recipe). Shape-/rank-polymorphic: the last-axis
/// sum reduces `SumDim(axis_last)` and the keepdim is restored by
/// `Unsqueeze(axis_last = append)` (the D3 shrink-via-swap replacing the baked
/// `ReduceSumTo(keepdim)`); the broadcast targets `SameAs { operand: 0 }` —
/// `s`'s full shape over the Bind space (D2). Bind: `0 = s` (the forward
/// softmax output), `1 = g` (the upstream gradient) — the order the autograd
/// edge emits (`lib.rs` softmax-backward arm: `vec![id, up_id]`). Nothing in
/// the datum bakes a shape or a rank; there are no open scalar slots
/// (parameterless).
///
/// Emitted form (6 op nodes; the `Unsqueeze` rebuilds rank BEFORE the
/// broadcast, so no D4 leading-1 pad is materialized):
///
/// ```text
/// gs = Mul(g, s) # g · s
/// summed = SumDim(axis_last)(gs) # shrink…
/// summed_kd = Unsqueeze(axis_last)(summed) # …restore keepdim ([..., 1])
/// summed_b = BroadcastTo(SameAs 0)(summed_kd)
/// sub = Sub(g, summed_b) # g − sum(g·s)
/// out = Mul(s, sub) # s · (…)
/// ```
/// Per-entry scalar projection: SoftmaxLastDimBackward is parameterless, so the
/// right payload projects to ZERO open-slot scalars and any other payload is a
/// typed decline (`None` ⇒ the bridge returns the node unchanged — G2).
/// Decompose to the closed-form softmax backward
/// `grad_x = s · (g − sum(g·s, last_dim, keepdim=true))`, where `s` (input 0)
/// is the forward output and `g` (input 1) is the upstream gradient — since T8
/// a re-emit of `recipe`'s data through the [`decompose_via_recipe`] bridge
/// (the fused node's two inputs `[s, g]` are the binds; the resolving emit
/// derives every interior shape/dtype). Any failure — wrong params payload, a
/// resolution decline at these shapes (symbolic extent, …) — returns `id`
/// (fixpoint, surfaced gap, never a panic): exactly the G2 posture the
/// imperative body had.
/// Matcher stub — backward-helper nodes originate from autograd
/// emitting `Op::Fused(SOFTMAX_LAST_DIM_BACKWARD, _)`, not from
/// user-decomposed forms.