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
//! **THE TWO NORMS INSIDE THE BLOCKS** -- BitNet's `attn_sub_norm` and
//! `ffn_sub_norm`, as one fact a `ModelConfig` carries, one table that
//! says which architecture has them, and one loader for the pair.
//!
//! # What they are
//!
//! A generic decoder layer has four norm sites, all OUTSIDE the two
//! sublayers (`crate::norm_sites`): before and after attention, before
//! and after the FFN. `src/models/bitnet.cpp` adds two INSIDE them:
//!
//! ```text
//! attn: x -> attn_norm -> qkv -> rope -> attend -> [attn_sub_norm] -> wo
//! ffn : x -> ffn_norm -> gate, up -> silu(gate) * up -> [ffn_sub_norm] -> down
//! ```
//!
//! `bitnet.cpp:24` creates `attn_sub_norm` `{n_embd}` REQUIRED and
//! `:101-106` applies it, RMS, to the attention output -- the
//! concatenated heads after the softmax-weighted V sum -- and THEN
//! `:107` runs `wo`. That is the other side of a matmul from Gemma's
//! `post_attention_norm`, which `AttnWeights::post_attn_norm` applies
//! AFTER `wo`; reading one as the other moves a norm across a
//! projection. `:36` creates `ffn_sub_norm` `{n_ff}` REQUIRED, `:127-132`
//! call `build_ffn` with a NULL down projection so it returns the
//! `silu(gate) * up` product, `:135-140` norm that, and `:141` apply
//! `ffn_down` by hand. The LM head is `tok_embd` unconditionally
//! (`:164`; `:14-17` create no `output` tensor), which is the tied
//! embedding frink already takes when `output.weight` is absent.
//!
//! # Reach -- MEASURED, not read off one file
//!
//! `grep -l 'attn_sub_norm\|ffn_sub_norm' src/models/*.cpp` over all
//! 155 graphs (2026-09-12) is `bitnet.cpp`. `LLM_TENSOR_ATTN_SUB_NORM`
//! and `LLM_TENSOR_FFN_SUB_NORM` (`llama-arch.cpp:510-511`) are created
//! by no other `load_arch_tensors`. So [`SUB_NORM_ARCHS`] has one row,
//! and the fact is a `bool` rather than an enum: there is no second
//! shape to name, and a variant with no caller is the OLMo lesson
//! (`capability::WEIGHTED_LAYER_NORM`).
//!
//! # Why a model-wide fact and not two `Option`s alone
//!
//! The two tensors live on the layer (`AttnWeights::attn_sub_norm`,
//! `MoeWeights::ffn_sub_norm`), because that is where the arithmetic
//! reads them. But every fused Metal launch is admitted by
//! `Decoder::metal_can_serve_model`, which takes the CONFIG, and none
//! of those kernels has a norm between attention and `wo` or between
//! the activation and `down`. So the architecture's answer is resolved
//! once into `ModelConfig::block_sub_norms`, the loader reads it to
//! decide whether the tensors are REQUIRED, and the Metal predicate
//! reads it to refuse. One fact, two readers; a file whose tensors and
//! architecture disagree is refused either way -- missing tensors on a
//! `bitnet` file by name, unread tensors on any other by the
//! unconsumed-tensor gate.
//!
//! # Where they are applied
//!
//! `attn_sub_norm` in `Decoder::attn_out_to_residual_rows`, the ONE
//! tail every host attention body ends in, after the output gate (no
//! graph has both; the order is documented there) and before `o_proj`.
//! `ffn_sub_norm` in `frink_moe::run_expert_sub_normed` for a row and
//! in `Decoder::dense_ffn_batch` for a batch, which is every dense FFN
//! body there is; the routed-expert bodies never see it because no MoE
//! graph has one (`build_moe_ffn` has no such site), and the loader
//! refuses the pair on a MoE layer by construction of the table.
//!
//! # What frink does NOT do that llama.cpp does on a BitNet file
//!
//! `bitnet.cpp:27-43` also create OPTIONAL per-tensor `blk.N.<proj>.scale`
//! tensors that `build_lora_mm` multiplies each projection's output by
//! (`llama-graph.cpp:1492-1494`), and since `llama-model.cpp:1355-1400`
//! every architecture's loader picks them up. The current converter
//! writes none (`conversion/bitnet.py:23-32` folds the scale into the
//! ternary weights); older exports carry them, and libllama's logits
//! MOVE when they are present (measured: the two fixtures differ at the
//! first logit). frink does not apply them and refuses such a file by
//! name (`crate::weight_scales`), rather than running it at the wrong
//! scale under the unread-tensor gate's `FRINK_ALLOW_UNKNOWN_TENSORS`.
use crateload_f32_vec;
use crateLoadError;
use TensorSource;
/// Architectures whose blocks carry the two inner norms, with the
/// graph lines that create and apply them.
pub const SUB_NORM_ARCHS: & =
&;
/// Whether this architecture's blocks norm INSIDE the two sublayers.
/// Layer `l`'s two inner norm weights, for a model whose config says it
/// has them: `attn_sub_norm` at `hidden_dim` and `ffn_sub_norm` at the
/// layer's FFN width, both REQUIRED as `bitnet.cpp:24,36` require them.
///
/// `None` for every other model, without touching the file, so a
/// tensor of that name on an architecture whose graph has no such
/// site stays UNREAD and is refused as such.
/// One layer's pair, as the loader hands them out; the decoder keeps
/// each on the sublayer that reads it.