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
// SPDX-License-Identifier: MIT OR Apache-2.0
//! RmsNormLastDimBackward — fused backward helper for the
//! `RmsNormLastDim` forward. Phase 7.6 step 4; migrated to a portable
//! `PatternNode` DATA recipe in Increment C carriers (A1 — the first recipe
//! with a SHAPE-DERIVED scalar slot).
//!
//! Closed-form: `grad_x = r_rms · (upstream - x · s / (n · (mean_sq + eps)))`
//! where `r_rms = rsqrt(mean_sq + eps)`, `mean_sq = mean(x², last)`,
//! `s = sum(upstream · x, last)`, `n = last_dim_size`. 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 carriers, A1). Sibling of the
//! layer-norm-backward recipe: every last-axis reduce is `MeanDim`/`SumDim(
//! axis_last)` with the keepdim restored by `Unsqueeze(axis_last = append)`
//! (the D3 shrink-via-swap replacing the baked `Reshape(keepdim)`), and each
//! broadcast targets `SameAs { operand: 0 }` (x's full shape, D2). The `eps`
//! `AddScalar` is an OPEN slot; the reduced-count divisor `n = dims[last]` is
//! a `MulScalar(scalar_rel = Extent(0, LAST))` — filled from x's shape at emit
//! time, NOT from the params projection (the A1 carrier).
//! - `scalars` — the per-entry projection `RmsNormLastDimBackward { 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(RMS_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 ;
use ;
use OnceLock;
/// Metadata-side registry entry for RmsNormLastDimBackward.
/// Shape rule: output equals input 0 (the forward rms-norm input `x`).
/// Dtype rule: output dtype equals input 0.
/// RmsNormLastDimBackward's primitive recipe as **portable data** (Increment C
/// carriers, A1). Shape-/rank-polymorphic: every last-axis reduce is
/// `MeanDim`/`SumDim(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 = up` (the upstream
/// gradient) — the order the autograd `BackwardKind::Fused` edge emits
/// (`inputs = [x, upstream]`).
///
/// Two kinds of non-baked scalar ride this datum:
/// * the `eps` `AddScalar` carries EMPTY `scalars` — an OPEN slot filled at emit
/// time from [`scalars`]'s params projection.
/// * the reduced-count divisor `n = dims[last]` is a `MulScalar(scalar_rel =
/// Extent(0, LAST))` — filled at emit time from x's SHAPE (the A1 carrier),
/// NOT from the params cursor, so it is never counted as an open slot.
///
/// D4 never fires: the `Unsqueeze` rebuilds rank BEFORE every broadcast, so no
/// leading-1 pad `Reshape` is materialized (equal rank throughout).
///
/// Emitted form (`reduce_kd(tag, v) = Unsqueeze(append)(tag(v))`,
/// `bcast(v) = BroadcastTo(SameAs 0)(v)`):
///
/// ```text
/// denom_kd = AddScalar[open slot](reduce_kd(MeanDim, Sqr(x))) # mean(x²)+eps
/// rrms_b = bcast(Rsqrt(denom_kd))
/// s_b = bcast(reduce_kd(SumDim, Mul(up, x))) # sum(up·x)
/// ndenom_b = bcast(MulScalar[n = Extent(0,LAST)](denom_kd)) # n·denom
/// term = Div(Mul(x, s_b), ndenom_b) # x·s / (n·denom)
/// grad_x = Mul(rrms_b, Sub(up, term)) # r_rms·(g − term)
/// ```
///
/// **RISK-A (accepted redundancy, documented — no emit extension in this
/// slice).** `denom_kd` carries the eps OPEN slot, so the emitter's slot-free
/// identity-share does NOT dedup it: it occurs at TWO semantic use sites
/// (`rrms_b`'s `Rsqrt` and `ndenom_b`'s `MulScalar`), so the datum tree holds
/// TWO `AddScalar` open slots and the emitted base map re-computes
/// `mean_sq + eps` per occurrence (heavier than the let-shared imperative DAG,
/// which shares the single `denom_kd`). This is numerically identical
/// (deterministic recompute) and the downstream full-optimizer CSE re-collapses
/// the duplicates back to the shared DAG. An open-slot subtree-sharing emit
/// extension is deliberately out of scope for this slice (a flagged future
/// extension). Mirrors the layer-norm-backward recipe's RISK-A posture.
/// Per-entry scalar projection: RmsNormLastDimBackward's open slots are all the
/// `eps` term (one per `AddScalar` occurrence in the datum — see the RISK-A note
/// on [`recipe`]; the reduced-count `MulScalar` is a shape-derived `scalar_rel`,
/// NOT an open slot, so it is excluded from the count). 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 closed-form RMSNorm backward
/// `grad_x = r_rms · (g − x·s / (n·(mean_sq + eps)))` — since A1 a re-emit of
/// `recipe`'s data through the [`decompose_via_recipe`] bridge (the fused
/// node's two inputs `[x, up]` are the binds; `scalars` fills the eps open
/// slots; the `MulScalar(n)` divisor resolves from x's shape via the
/// `scalar_rel` carrier; 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(RMS_NORM_LAST_DIM_BACKWARD, _)`, not from user-decomposed forms.