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
// SPDX-License-Identifier: MIT OR Apache-2.0
//! LayerNormLastDimBackward — fused backward helper for the
//! `LayerNormLastDim` forward. Phase 7.6 step 4; migrated to a portable
//! `PatternNode` DATA recipe in Increment C slice 2 (S2-1).
//!
//! Recomputes mean/variance from the original x (rather than caching
//! intermediates from forward) and produces
//! `grad_x = istd · (g − mean(g) − xhat·mean(g·xhat))`, where `mean(·)` is over
//! the last dim, `xhat = (x − mean(x))·istd`, and `istd = rsqrt(var + eps)`.
//! Two inputs `[x, upstream]` + `eps`.
//!
//! Provides:
//! - [`entry`] — the metadata-side `FusedOpEntry`.
//! - `recipe` — the op's closed-form backward as portable, shape-/rank-
//! polymorphic `PatternNode` data (Increment C slice 2). The four keepdim
//! restores are the D3 shrink-via-swap (`MeanDim(axis_last)` + `Unsqueeze(
//! axis_last = append)`, replacing the baked `Reshape(keepdim)`) and each
//! broadcast targets `SameAs { operand: 0 }` (x's full shape, D2). One `eps`
//! `AddScalar` OPEN slot, filled from the params projection.
//! - `scalars` — the per-entry projection `LayerNormLastDimBackward { eps } →
//! vec![eps; open-slot-count]` (one eps per open slot — see the RISK-A note
//! on `recipe`).
//! - [`decompose`] — re-emits `recipe` through the
//! [`crate::registry::decompose_via_recipe`] bridge.
//!
//! See `softmax_last_dim_backward.rs` for the architectural rationale shared by
//! all four backward-helper entries: the registry's role is identity +
//! `BackwardKind::Fused` target + future `BackendImpl` host, and the matcher is
//! always stubbed (backward-helper nodes originate from autograd emitting
//! `Op::Fused(LAYER_NORM_LAST_DIM_BACKWARD, _)`, not from user-decomposed
//! forms). The `decompose` IS a "synthesize from primitives" recipe — every op
//! is a functional primitive, so per G2 it decomposes totally, never a
//! basis-gap self-return.
use crate;
use crate::;
use ;
use ShapeExpr;
use ;
use OnceLock;
/// Metadata-side registry entry for LayerNormLastDimBackward.
/// Shape rule: output equals input 0 (the forward layer-norm input `x`).
/// Dtype rule: output dtype equals input 0.
/// LayerNormLastDimBackward's primitive recipe as **portable data** (Increment
/// C slice 2, S2-1). Shape-/rank-polymorphic: every last-axis reduce is
/// `MeanDim(axis_last)` and the keepdim is restored by `Unsqueeze(axis_last =
/// append)` (the D3 shrink-via-swap replacing the baked `Reshape(keepdim)`);
/// each broadcast targets `SameAs { operand: 0 }` — x's full shape over the
/// Bind space (D2). Binds: `0 = x`, `1 = g` (the upstream gradient) — the order
/// the autograd `BackwardKind::Fused` edge emits (`inputs = [x, upstream]`).
///
/// The `eps` `AddScalar` carries EMPTY `scalars` — an OPEN slot filled at emit
/// time from [`scalars`]'s projection. D4 never fires: the `Unsqueeze` rebuilds
/// rank BEFORE every broadcast, so no leading-1 pad `Reshape` is materialized
/// (equal rank throughout).
///
/// Emitted form (`mean_b(v) = BroadcastTo(SameAs 0)(Unsqueeze(MeanDim(v)))`):
///
/// ```text
/// xc = Sub(x, mean_b(x)) # centered
/// var_eps = AddScalar[open slot](mean_b(Sqr(xc)))
/// istd = Rsqrt(var_eps)
/// xhat = Mul(xc, istd)
/// g_xhat = Mul(g, xhat)
/// t1 = Sub(g, mean_b(g))
/// t2 = Mul(xhat, mean_b(g_xhat))
/// grad_x = Mul(istd, Sub(t1, t2)) # = istd·(g − mean(g) − xhat·mean(g·xhat))
/// ```
///
/// **RISK-A (accepted redundancy, documented — no emit extension in this
/// slice).** `xhat` (and its `istd`) carry the eps OPEN slot, so the emitter's
/// slot-free identity-share does NOT dedup them: `istd` occurs at three
/// semantic use sites (`grad_x`, and each of `xhat`'s two consumers) and `xhat`
/// at two, so the datum tree holds THREE `AddScalar` open slots and the emitted
/// base map re-computes `xhat`/`istd`/`var_eps` per occurrence (heavier than the
/// let-shared imperative DAG — verified 27 vs 22 primitive nodes). This is
/// numerically identical (deterministic recompute) and the downstream
/// full-optimizer CSE re-collapses the duplicates back to the shared DAG
/// (both verified in `runtime_fused`'s
/// `layer_norm_backward_recipe_open_slot_xhat_is_recomputed_then_cse_recollapses`).
/// An open-slot subtree-sharing emit extension is deliberately out of scope for
/// this slice (a flagged future extension).
/// Per-entry scalar projection: LayerNormLastDimBackward's open slots are all
/// the `eps` term (one per `AddScalar` occurrence in the datum — see the RISK-A
/// note on [`recipe`]), so the right payload projects to `vec![eps; slots]` and
/// any other payload is a typed decline (`None` ⇒ the bridge returns the node
/// unchanged — G2). Deriving the count from [`recipe`]'s open-slot count keeps
/// the projection in lockstep with the datum's structure (all slots take the
/// same eps value, so pre-order alignment is trivially satisfied).
/// Decompose to the affine-free LayerNorm backward
/// `grad_x = istd · (g − mean(g) − xhat·mean(g·xhat))` — since S2-1 a re-emit of
/// `recipe`'s data through the [`decompose_via_recipe`] bridge (the fused
/// node's two inputs `[x, g]` are the binds; `scalars` fills the eps open
/// slots; 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(LAYER_NORM_LAST_DIM_BACKWARD, _)`, not from user-decomposed forms.