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
//! **WHICH TENSOR THE MoE ROUTER READS** -- the operand of
//! `ffn_gate_inp`, as one value a `ModelConfig` carries and one table
//! that says which architecture reads what.
//!
//! # What it is
//!
//! llama.cpp's `build_moe_ffn` (`llama-graph.cpp:1914-1948`) computes
//! the router logits itself, `logits = gate_inp · cur`, from the SAME
//! `cur` the experts then read -- the normed FFN input -- UNLESS the
//! caller hands it a precomputed `probs_in`, in which case `gate_inp`
//! is unused and the caller decided the operand. Every frink MoE body
//! computed `router · normed2`, which is the default and right for
//! every graph that takes it.
//!
//! # Who passes `probs_in` -- MEASURED, not read off one file
//!
//! Every `build_moe_ffn(` call in all 155 `src/models/*.cpp` was parsed
//! for its `gate_inp` and `probs_in` arguments (2026-09-11). Fifty-nine
//! call sites; four pass a precomputed `probs_in`:
//!
//! | arch | router operand | why precomputed | engine here | line |
//! |---|---|---|---|---|
//! | `smallthinker` | `inpL` -- the RAW LAYER INPUT, before `attn_norm`, before attention | the operand is different | generic GQA | `smallthinker.cpp:111,151-161` |
//! | `grovemoe` | `cur` -- the normed FFN input, the default | shared between TWO `build_moe_ffn` calls (the expert bank and the chunk-expert bank) | generic GQA, refused for the second bank | `grovemoe.cpp:133,137-148,153-164` |
//! | `gemma4` | `rms_norm(attn_out) * (1/sqrt(n_embd)) * ffn_gate_inp_s` -- the attention output, its own norm, a scale tensor | the operand is different | its own engine (`gemma4_engine`) | `gemma4.cpp:289-294` |
//! | `nemotron-h` | `cur` -- the FFN input BEFORE the latent down-projection the experts read | the experts read `inp_latent`, the router does not | hybrid recurrent engine | `nemotron-h.cpp:210-232` |
//!
//! Two more route on something other than a variable named `cur` and
//! are the default anyway: `llama4.cpp:221` passes `ffn_inp_normed`
//! (the normed FFN input) and `cohere2moe.cpp:234,389` pass `ffn_inp`
//! (the parallel-residual topology's one normed input, which its
//! experts read too). Fifty-three sites pass `nullptr` or the 13-arg
//! overload and route on `cur`.
//!
//! So `smallthinker` is the ONLY generic-path graph whose router
//! operand is not what the experts read, and [`RouterInput`] had two
//! variants rather than four: `gemma4`'s and `nemotron-h`'s shapes
//! live on engines that do not read this field, and a variant with no
//! caller is the OLMo lesson (`capability::WEIGHTED_LAYER_NORM`).
//! `grovemoe` shares the mechanism (a precomputed `probs`) and NOT the
//! cause; that is why the table is keyed by what the router reads and
//! not by whether `probs_in` is non-null.
//!
//! # The third variant: a different `cur` -- `arctic`
//!
//! `arctic.cpp:135-152` passes NO `probs_in`; its router reads `cur`,
//! the default mechanism. What differs is `cur` itself:
//! `build_norm(inpSA, ffn_norm_exps)` at `:136-139` -- the residual
//! stream as it ENTERS the layer (`inpSA = inpL`, `:69`), before
//! attention, normed by a SECOND per-layer weight -- and the routed
//! experts read that same vector, while the layer's dense FFN
//! (`:118-132`, `crate::parallel_dense_ffn`) reads the ordinary
//! `ffn_norm(ffn_inp)`. `grep -l FFN_NORM_EXPS src/models/*.cpp` over
//! all 155 graphs is `arctic.cpp` (2026-09-12), so
//! [`RouterInput::NormedLayerInput`] has one row and carries the fact
//! that distinguishes it from `smallthinker`'s: the EXPERTS read it
//! too ([`RouterInput::experts_read_router_operand`]). The bodies
//! capture it at the same point as `smallthinker`'s logits -- where
//! `attn_norm` is applied, before attention -- through
//! `Decoder::router_operand`, and every fused Metal MoE launch refuses
//! it through the predicate that already refused `RawLayerInput`.
//!
//! # What `inpL` is, exactly
//!
//! `smallthinker.cpp:86` sets `inpL = build_inp_embd(...)` and `:172`
//! sets `inpL = cur` at the bottom of every layer, so at layer `il` it
//! is the residual stream as it ENTERS the layer: the scaled embedding
//! row at layer 0, the previous layer's output after both residual
//! adds otherwise. `:111` reads it BEFORE `:115` norms it for
//! attention, so the router sees no norm at all. frink captures it at
//! the same point (`Decoder::router_operand`, called where the row's
//! `attn_norm` is applied) and computes the logits there, in the same
//! order llama.cpp does, so the operand cannot be the post-attention
//! residual by mistake.
//!
//! Everything downstream of the logits is the ordinary
//! `build_moe_ffn` (`:151-161`): `expert_gating_func` from the file
//! (`conversion/smallthinker.py:27-30` writes SOFTMAX or SIGMOID),
//! `norm_w = true` as a literal, `expert_weights_scale` unset
//! (skipped at 0), no `exp_probs_b`, no shared expert, no groups.
//! `Decoder::route_for_layer` already implements all of that.
//!
//! # Where it is served, and where it refuses
//!
//! The CPU row body and both batched host bodies take the operand
//! from `Decoder::router_operand`, ONE function, and hand it to the
//! ONE FFN body per shape (`decoder/ffn_block.rs`). Every Metal path
//! that runs the router on the GPU reads `normed2` and nothing else,
//! so `Decoder::gpu_router_matches_host_routing` -- the predicate all
//! of them already share -- answers false for [`RouterInput::
//! RawLayerInput`], and those launches fall back to the host bodies
//! rather than routing on the wrong tensor.
/// The operand of the MoE router's matmul.
/// Which operand each architecture's router reads. The table behind
/// the census above, restricted to the generic path; the two rows on
/// other engines are documented there and not here, because nothing
/// on those engines asks this question.
pub const ROUTER_INPUT_TABLE: & = &;
/// The router operand for an architecture: the table's entry, or the
/// default for every architecture the table does not name.