nx_llm_run_v2.nx source
↩ module page · 217 lines · 9214 B
1// nx_llm_run_v2.nx -- end-to-end Llama-class inference composer (real
2// weights edition).
3//
4// L6 brick. The CLOSER for the inference path: composes every loader
5// + per-layer compute + sampler primitive shipped this session into a
6// single callable that maps:
7//
8// text prompt + GGUF + BPE vocab + sampling knobs -> next-token-id
9//
10// Compared to nx_llm_run.nx (v1, scaffold-only):
11// * v1 omitted the per-layer transformer compute (no weight loader)
12// * v1 simulated output projection as identity copy
13// * v2 sources ALL weights from a parsed GGUF and runs the full
14// forward stack
15//
16// Per the bits-up + no-skipping cardinals: no new math, no reinvented
17// kernels. Pure composition of:
18//
19// nx_bpe.nx_bpe_encode -- text -> token ids
20// nx_gguf_load_model.nx_..._weights -- top-level tensor bundle
21// nx_tensor.nx_t_alloc -- working buffers
22// nx_embedding.nx_embedding_lookup -- token-id -> hidden vector
23// nx_transformer_stack.nx_..._fwd -- N-layer forward
24// nx_blas_i64.nx_blas_matmul -- output projection to vocab
25// nx_token_sample.* + nx_attention.nx_attn_softmax_row_q10
26// -- temp + top_k + softmax + sample
27//
28// genealogy_id: standard_decoder_only_llm_inference +
29// touvron_2023_llama + radford_2019_gpt2
30// lineage_id: substrate_llm_run_v2_real_weights
31
32// nx_safety_envelope:
33// intended_use: "End-to-end Llama-class next-token generation
34// from a parsed GGUF; single-prompt scope; no
35// KV cache reuse across calls (v3 work); no
36// multi-head (single-head v1)"
37// sil_target: SIL2
38// asil_target: QM
39// dal_target: DAL C
40// evidence: [composes_only_shipped_canonical_bricks,
41// each_loader_smoke_green, stack_smoke_green]
42// hazard_register: [bug-tape-tied-embed-no-transpose,
43// bug-tape-vocab-mismatch-vs-bpe,
44// bug-tape-attn-scale-default,
45// bug-tape-overflow-output-projection]
46// residual_risk: "Tied-embedding GGUFs (Llama-2/3) are
47// REJECTED in v2 -- v3 adds matmul-with-
48// transpose-view to handle them"
49// verdict: NOT_YET_EVALUATED
50
51import "nx_syscalls.nx"
52import "nx_tier.nx"
53import "nx_loop.nx"
54import "nx_tensor.nx"
55import "nx_bpe.nx"
56import "nx_embedding.nx"
57import "nx_blas_i64.nx"
58import "nx_attention.nx"
59import "nx_token_sample.nx"
60import "nx_model_spec.nx"
61import "nx_prng.nx"
62import "nx_gguf.nx"
63import "nx_gguf_load.nx"
64import "nx_gguf_load_model.nx"
65import "nx_transformer_stack.nx"
66
67// ===== Sealed-enum: LlmRunV2Verdict ================================
68
69const NX_LR2_OK: nx_int = 0
70const NX_LR2_ERR_BAD_SPEC: nx_int = 1
71const NX_LR2_ERR_TOKENIZE: nx_int = 2
72const NX_LR2_ERR_MODEL_LOAD: nx_int = 3
73const NX_LR2_ERR_EMBED: nx_int = 4
74const NX_LR2_ERR_STACK_FWD: nx_int = 5
75const NX_LR2_ERR_TIED_NOT_SUPPORTED: nx_int = 6
76const NX_LR2_ERR_OOM: nx_int = 7
77const NX_LR2_ERR_BAD_ROPE: nx_int = 8
78const NX_LR2_N_VERDICTS: nx_int = 9
79
80func nx_lr2_verdict_is_valid(v: nx_int) -> nx_int {
81 if v < 0 { return 0 }
82 if v >= NX_LR2_N_VERDICTS { return 0 }
83 return 1
84}
85
86// Compute 1/sqrt(d) in Q10 with d small (head_dim typically 64-128).
87// For our v1 single-head usage with tiny d (smoke uses d=2), we use
88// the substrate's existing integer-sqrt path. Output is Q10.
89//
90// 1/sqrt(d) Q10 = Q10 * Q10 / sqrt(d * Q20) approximately
91//
92// Simpler exact-when-d-small recipe used here:
93// sqrt_d_q10 = nx_isqrt(d * Q20) (NOT shipped under that name) --
94// instead: for d=2, 1/sqrt(2) = 0.7071 -> Q10 = 724
95// for d=128, 1/sqrt(128) = 0.0884 -> Q10 = 91
96//
97// The substrate already has nx_isqrt; we use it iff available.
98// For this v2 brick, caller passes attn_scale_q10 directly so this
99// helper is not needed. Comment retained for the v3 derivation.
100
101// ===== Public: generate one next token ============================
102//
103// Args (12 -- under 16-arg limit):
104// spec model architecture envelope
105// gguf_buf, hdr parsed GGUF (caller pre-parses)
106// bpe BPE vocab (caller pre-loads; v3 reads from GGUF metadata)
107// prompt_text, prompt_len UTF-8 prompt bytes
108// temperature_q10 sampling temperature (Q10; 1024 = 1.0)
109// top_k top-K mask (e.g. 40)
110// prng_state *i64 mutable PRNG state
111// rope_base RoPE theta base (10000 / 500000)
112// attn_scale_q10 attention scale 1/sqrt(head_dim) in Q10
113//
114// Returns: next token ID (>= 0) on success, or 0 - verdict_id on error.
115
116func nx_llm_generate_one_v2(
117 spec: *NxModelSpec,
118 gguf_buf: *u8, hdr: *NxGgufHeader,
119 bpe: *NxBpeVocab,
120 prompt_text: *u8, prompt_len: nx_int,
121 temperature_q10: nx_int, top_k: nx_int,
122 prng_state: *i64,
123 rope_base: nx_int, attn_scale_q10: nx_int) -> nx_int {
124
125 // Boundary null guards -- public-API runner must reject null inputs
126 // cleanly instead of SEGVing inside the encode/load steps. Composes
127 // with the four-pillar null-deref preventative scan.
128 if (spec as i64) == 0 { return 0 - NX_LR2_ERR_BAD_SPEC }
129 if (gguf_buf as i64) == 0 { return 0 - NX_LR2_ERR_MODEL_LOAD }
130 if (hdr as i64) == 0 { return 0 - NX_LR2_ERR_MODEL_LOAD }
131 if (bpe as i64) == 0 { return 0 - NX_LR2_ERR_TOKENIZE }
132 if (prompt_text as i64) == 0 { return 0 - NX_LR2_ERR_TOKENIZE }
133 if (prng_state as i64) == 0 { return 0 - NX_LR2_ERR_BAD_SPEC }
134 if prompt_len <= 0 { return 0 - NX_LR2_ERR_TOKENIZE }
135 if temperature_q10 <= 0 { return 0 - NX_LR2_ERR_BAD_SPEC }
136 if top_k <= 0 { return 0 - NX_LR2_ERR_BAD_SPEC }
137 if attn_scale_q10 <= 0 { return 0 - NX_LR2_ERR_BAD_SPEC }
138
139 let v_spec: nx_int = nx_model_spec_validate(spec)
140 if v_spec != NX_MS_OK { return 0 - NX_LR2_ERR_BAD_SPEC }
141 if rope_base <= 1 { return 0 - NX_LR2_ERR_BAD_ROPE }
142
143 // ----- Step 1: tokenize prompt -----
144 let token_buf: *i64 = sys_mmap((prompt_len + 16) * 8) as *i64
145 let n_tokens: nx_int = nx_bpe_encode(bpe, prompt_text, prompt_len, token_buf)
146 if n_tokens <= 0 { return 0 - NX_LR2_ERR_TOKENIZE }
147 if n_tokens > spec.max_seq_len { return 0 - NX_LR2_ERR_BAD_SPEC }
148
149 // ----- Step 2: load model-level weights -----
150 let mw: *NxGgufModelWeights = sys_mmap(NX_GML_BUNDLE_BYTES) as *NxGgufModelWeights
151 let err: *i64 = sys_mmap(8) as *i64
152 err[0] = 0
153 let v_ml: nx_int = nx_gguf_load_model_weights(gguf_buf, hdr, mw, err)
154 if v_ml != NX_GML_OK { return 0 - NX_LR2_ERR_MODEL_LOAD }
155 if mw.is_output_tied == 1 {
156 // v3 path adds transposed-view matmul. v2 rejects.
157 return 0 - NX_LR2_ERR_TIED_NOT_SUPPORTED
158 }
159
160 // ----- Step 3: embed -----
161 let sh_x: *i64 = sys_mmap(2 * 8) as *i64
162 sh_x[0] = n_tokens; sh_x[1] = spec.hidden_dim
163 let x: *NxTensor = nx_t_alloc(NX_DT_I64, sh_x, 2, err)
164 if err[0] != 0 { return 0 - NX_LR2_ERR_OOM }
165 let v_emb: nx_int = nx_embedding_lookup(mw.token_embd, token_buf, n_tokens, x)
166 if v_emb != NX_EMB_OK { return 0 - NX_LR2_ERR_EMBED }
167
168 // ----- Step 4: positions [n_tokens] -----
169 let positions: *i64 = sys_mmap(n_tokens * 8) as *i64
170 var p_i: nx_int = 0
171 while p_i < n_tokens {
172 positions[p_i] = p_i
173 p_i = p_i + 1
174 }
175
176 // ----- Step 5: transformer stack forward -----
177 let v_st: nx_int = nx_transformer_stack_forward(
178 x, positions, gguf_buf, hdr,
179 spec.n_layers, spec.hidden_dim, spec.head_dim, spec.ffn_dim,
180 rope_base, attn_scale_q10,
181 mw.output_norm_gamma)
182 if v_st != NX_TS_OK { return 0 - NX_LR2_ERR_STACK_FWD }
183
184 // ----- Step 6: extract last-token hidden vector as a [1, hidden] tensor -----
185 let sh_h: *i64 = sys_mmap(2 * 8) as *i64
186 sh_h[0] = 1; sh_h[1] = spec.hidden_dim
187 let hidden_last: *NxTensor = nx_t_alloc(NX_DT_I64, sh_h, 2, err)
188 if err[0] != 0 { return 0 - NX_LR2_ERR_OOM }
189 let phl: *i64 = hidden_last.storage as *i64
190 let px: *i64 = x.storage as *i64
191 let base: nx_int = (n_tokens - 1) * spec.hidden_dim
192 var c: nx_int = 0
193 while c < spec.hidden_dim {
194 phl[c] = px[base + c]
195 c = c + 1
196 }
197
198 // ----- Step 7: output projection -- matmul([1, hidden], [hidden, vocab]) -----
199 let sh_l: *i64 = sys_mmap(2 * 8) as *i64
200 sh_l[0] = 1; sh_l[1] = spec.vocab_size
201 let logits_t: *NxTensor = nx_t_alloc(NX_DT_I64, sh_l, 2, err)
202 if err[0] != 0 { return 0 - NX_LR2_ERR_OOM }
203 nx_blas_matmul(hidden_last, mw.output_weight, logits_t)
204
205 let logits: *i64 = logits_t.storage as *i64
206
207 // ----- Step 8: temperature + top-K mask -----
208 nx_logit_apply_temperature(logits, spec.vocab_size, temperature_q10)
209 nx_logit_top_k_mask(logits, spec.vocab_size, top_k)
210
211 // ----- Step 9: softmax (treat logits as [1, vocab] tensor) -----
212 nx_attn_softmax_row_q10(logits_t)
213
214 // ----- Step 10: sample -----
215 let next_token: nx_int = nx_sample_categorical(logits, spec.vocab_size, prng_state)
216 return next_token
217}