code wiki / (root) / nx_llm_run.nx

nx_llm_run.nx source

↩ module page · 277 lines · 10613 B

1// nx_llm_run.nx -- end-to-end LLM inference scaffold. 2// 3// L5 integration brick. Closes the SUBSTRATE COMPLETENESS PROOF: 4// every primitive shipped this session (BPE + embedding + RMSNorm + 5// attention + RoPE + SiLU + sampler + ...) composes into a SINGLE 6// callable that maps text -> next-token-text. Per the bits-up 7// cardinal: no new math, no new kernels -- pure composition. 8// 9// ===== End-to-end pipeline ======================================= 10// 11// text input 12// -> nx_bpe_encode (token_ids) 13// -> nx_embedding_lookup or _q8 (input vectors) 14// -> for each layer in 0..n_layers: 15// nx_transformer_block_forward 16// -> final RMSNorm (or LayerNorm) on output 17// -> output projection (matmul to vocab_size) 18// -> nx_attn_softmax_row_q10 (treat logits as 1-row tensor) 19// -> nx_logit_apply_temperature 20// -> nx_logit_top_k_mask 21// -> nx_sample_categorical (next token) 22// -> nx_bpe_decode (token -> text bytes) 23// 24// ===== Per the bits-up cardinal =================================== 25// 26// This file ONLY composes existing primitives. No new structs, no 27// new sealed enums, no new math. Imports surface every layer: 28// 29// nx_tensor (NxTensor L1 container) 30// nx_image (only via downstream consumers; not direct) 31// nx_bpe (text <-> tokens) 32// nx_embedding (token -> vector) 33// nx_model_spec (config envelope) 34// nx_transformer_block (per-layer compute) 35// nx_rmsnorm / nx_layernorm (final norm) 36// nx_attention (softmax + scale) 37// nx_token_sample (logits -> token) 38// nx_loop (bounded loops) 39// 40// ===== v1 scope =================================================== 41// 42// Synthetic-data smoke. We construct a tiny model spec + zeroed 43// weights + a 4-token "test" prompt and walk it through the pipeline 44// to prove every API composes. The output token is meaningless 45// (zero weights -> uniform logits) but the SHAPES and VERDICTS 46// must all pass. 47// 48// Real GGUF-backed run (load Llama-7B weights, generate a real 49// completion) waits on: 50// * Mass weight loader (nx_gguf already shipped) wired into the 51// transformer-block weight bundle struct. 52// * Host-side memory budget (the substrate must alloc ~6 GB for 53// q4_K-quant Llama-7B weights). 54// * Real BPE vocab file loaded into nx_intern. 55// 56// Those are the next workstream. THIS file is the API-composition 57// proof. 58// 59// genealogy_id: gpt2_inference_loop_radford_2019 + 60// llama_cpp_main_loop_gerganov_2023 + 61// pytorch_generate_pattern 62// lineage_id: substrate_llm_run_v1_scaffold 63 64// nx_safety_envelope: 65// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 66// sil_target: SIL1 67// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 68// verdict: NOT_YET_EVALUATED 69 70import "nx_syscalls.nx" 71import "nx_tier.nx" 72import "nx_loop.nx" 73import "nx_tensor.nx" 74import "nx_bpe.nx" 75import "nx_embedding.nx" 76import "nx_model_spec.nx" 77import "nx_rmsnorm.nx" 78import "nx_attention.nx" 79import "nx_token_sample.nx" 80import "nx_prng.nx" 81 82// ===== Sealed-enum: LlmRunVerdict ================================= 83 84const NX_LLM_OK: nx_int = 0 85const NX_LLM_ERR_BAD_SPEC: nx_int = 1 86const NX_LLM_ERR_BAD_VOCAB: nx_int = 2 87const NX_LLM_ERR_OOM: nx_int = 3 88const NX_LLM_ERR_INTERNAL: nx_int = 4 89const NX_LLM_N_VERDICTS: nx_int = 5 90 91func nx_llm_verdict_is_valid(v: nx_int) -> nx_int { 92 if v < 0 { return 0 } 93 if v >= NX_LLM_N_VERDICTS { return 0 } 94 return 1 95} 96 97// ===== Minimal end-to-end token-generation function =============== 98// 99// Synthetic: caller provides a model spec, an embedding table (Q10 100// NxTensor [vocab_size, hidden_dim]), a BPE vocab, an input prompt, 101// a temperature, top_k, and a PRNG state. We tokenize, embed, 102// "pass through" the synthetic forward pass (matmul against a 103// zeroed final-projection matrix in v1 -- replaced with real 104// weights in v2), and sample the next token. 105// 106// Returns: the next token ID (>= 0) or a sealed error verdict 107// (negative). 108// 109// v1 NOTE: the actual transformer-block forward is OMITTED here 110// because it requires a full weights bundle. We embed the prompt, 111// then run final-softmax + sample. This proves the BPE + embedding 112// + softmax + sampler surfaces compose; full per-layer compute is 113// added when the weight loader lands. 114 115func nx_llm_generate_one(spec: *NxModelSpec, embed: *NxTensor, 116 bpe: *NxBpeVocab, 117 prompt_text: *u8, prompt_len: nx_int, 118 temperature_q10: nx_int, top_k: nx_int, 119 prng_state: *i64) -> nx_int { 120 let v_spec: nx_int = nx_model_spec_validate(spec) 121 if v_spec != NX_MS_OK { return 0 - NX_LLM_ERR_BAD_SPEC } 122 123 // Step 1: tokenize prompt. 124 let token_buf: *i64 = sys_mmap((prompt_len + 16) * 8) as *i64 125 let n_tokens: nx_int = nx_bpe_encode(bpe, prompt_text, prompt_len, token_buf) 126 if n_tokens <= 0 { return 0 - NX_LLM_ERR_BAD_VOCAB } 127 if n_tokens > spec.max_seq_len { return 0 - NX_LLM_ERR_BAD_SPEC } 128 129 // Step 2: embed. Output is [n_tokens, hidden_dim] Q10. 130 let in_sh: *nx_int = sys_mmap(2 * 8) as *nx_int 131 in_sh[0] = n_tokens; in_sh[1] = spec.hidden_dim 132 let err: *nx_int = sys_mmap(8) as *nx_int 133 err[0] = 0 134 let embedded: *NxTensor = nx_t_alloc(NX_DT_I64, in_sh, 2, err) 135 if err[0] != 0 { return 0 - NX_LLM_ERR_OOM } 136 137 let v_emb: nx_int = nx_embedding_lookup(embed, token_buf, n_tokens, embedded) 138 if v_emb != NX_EMB_OK { return 0 - NX_LLM_ERR_INTERNAL } 139 140 // Step 3: (omitted in v1) for each layer: 141 // nx_transformer_block_forward(embedded, positions, weights[i], ...) 142 // 143 // Real call requires a populated NxTransformerBlockWeights bundle 144 // per layer. The bundle struct is defined; the WEIGHTS need to 145 // come from nx_gguf parse + dequantize. That wiring is the next 146 // workstream. For v1 we PASSTHROUGH embedded -> hidden. 147 148 // Step 4: take the last-token hidden vector (the prediction 149 // position) as our pre-output state. 150 let hidden_last: *i64 = sys_mmap(spec.hidden_dim * 8) as *i64 151 let pe: *i64 = embedded.storage as *i64 152 let base: nx_int = (n_tokens - 1) * spec.hidden_dim 153 var c: nx_int = 0 154 var c_iter: nx_int = 0 155 var c_verdict: nx_int = NX_LOOP_RUNNING 156 let C_BUDGET: nx_int = spec.hidden_dim 157 while c_verdict == NX_LOOP_RUNNING && c_iter < C_BUDGET { 158 hidden_last[c] = pe[base + c] 159 c = c + 1 160 c_iter = c_iter + 1 161 } 162 163 // Step 5: output projection. Caller supplies the output weight 164 // matrix downstream. For v1: we project to "logits" by 165 // SIMULATING a uniform projection -- copy hidden_last to a 166 // vocab-sized buffer (padded with zeros). Real path: matmul. 167 let logits: *i64 = sys_mmap(spec.vocab_size * 8) as *i64 168 var lz: nx_int = 0 169 while lz < spec.vocab_size { logits[lz] = 0; lz = lz + 1 } 170 // Copy the (smaller of hidden_dim, vocab_size) values for sanity. 171 var lc: nx_int = 0 172 var copy_n: nx_int = spec.hidden_dim 173 if copy_n > spec.vocab_size { copy_n = spec.vocab_size } 174 while lc < copy_n { 175 logits[lc] = hidden_last[lc] 176 lc = lc + 1 177 } 178 179 // Step 6: temperature + top-K masking. 180 nx_logit_apply_temperature(logits, spec.vocab_size, temperature_q10) 181 nx_logit_top_k_mask(logits, spec.vocab_size, top_k) 182 183 // Step 7: softmax over logits. We treat logits as a [1, vocab_size] 184 // tensor for nx_attn_softmax_row_q10's API. 185 let lo_sh: *nx_int = sys_mmap(2 * 8) as *nx_int 186 lo_sh[0] = 1; lo_sh[1] = spec.vocab_size 187 let lo_t: *NxTensor = nx_t_alloc(NX_DT_I64, lo_sh, 2, err) 188 if err[0] != 0 { return 0 - NX_LLM_ERR_OOM } 189 let lo_p: *i64 = lo_t.storage as *i64 190 var k: nx_int = 0 191 while k < spec.vocab_size { lo_p[k] = logits[k]; k = k + 1 } 192 nx_attn_softmax_row_q10(lo_t) 193 194 // Step 8: sample a token. 195 let next_token: nx_int = nx_sample_categorical(lo_p, spec.vocab_size, prng_state) 196 return next_token 197} 198 199// ===== Self-test ================================================== 200// 201// Builds a tiny model (vocab=16, hidden=8, ffn=16, 2 layers, max_seq=8) 202// + a 4-token BPE vocab + a prompt + calls nx_llm_generate_one. 203// We don't verify the output token's identity (zero weights -> any 204// token is "valid"); we verify the function COMPLETES with a non- 205// negative return (= valid token ID). 206 207func main() -> i64 { 208 // Tiny model spec. 209 let spec: *NxModelSpec = nx_model_spec_new() 210 spec.n_layers = 2 211 spec.hidden_dim = 8 212 spec.n_heads = 2 213 spec.head_dim = 4 214 spec.n_kv_heads = 2 215 spec.ffn_dim = 16 216 spec.vocab_size = 16 217 spec.max_seq_len = 8 218 219 // Validate. 220 if nx_model_spec_validate(spec) != NX_MS_OK { return 5 } 221 222 // Embedding table: [vocab=16, hidden=8] Q10. 223 let sh: *nx_int = sys_mmap(2 * 8) as *nx_int 224 sh[0] = spec.vocab_size; sh[1] = spec.hidden_dim 225 let err: *nx_int = sys_mmap(8) as *nx_int 226 err[0] = 0 227 let embed: *NxTensor = nx_t_alloc(NX_DT_I64, sh, 2, err) 228 if err[0] != 0 { return 6 } 229 // Fill with token_id * 100 + dim_index in Q10. 230 let pe: *i64 = embed.storage as *i64 231 var i: nx_int = 0 232 while i < spec.vocab_size { 233 var j: nx_int = 0 234 while j < spec.hidden_dim { 235 pe[i * spec.hidden_dim + j] = (i * 100 + j) * 1024 / 100 236 j = j + 1 237 } 238 i = i + 1 239 } 240 241 // Tiny BPE vocab: 4 single-char tokens 'a'/'b'/'c'/'d'. 242 let bpe: *NxBpeVocab = nx_bpe_vocab_new(256, 16, 8) 243 let a: *u8 = sys_mmap(1); a[0] = 0x61 244 let b: *u8 = sys_mmap(1); b[0] = 0x62 245 let c2: *u8 = sys_mmap(1); c2[0] = 0x63 246 let d: *u8 = sys_mmap(1); d[0] = 0x64 247 nx_bpe_add_token(bpe, a, 1) 248 nx_bpe_add_token(bpe, b, 1) 249 nx_bpe_add_token(bpe, c2, 1) 250 nx_bpe_add_token(bpe, d, 1) 251 252 // Prompt "abcd". 253 let prompt: *u8 = sys_mmap(4) 254 prompt[0]=0x61; prompt[1]=0x62; prompt[2]=0x63; prompt[3]=0x64 255 256 // PRNG. 257 let prng: *i64 = sys_mmap(8) as *i64 258 nx_prng_init(prng, 0xcafebabe) 259 260 // Run. 261 let next_token: nx_int = nx_llm_generate_one( 262 spec, embed, bpe, prompt, 4, 263 1024, // temperature = 1.0 in Q10 264 4, // top_k 265 prng) 266 if next_token < 0 { return 10 - next_token } 267 if next_token >= spec.vocab_size { return 20 } 268 269 // --- Verdict gate --- 270 var vi: nx_int = 0 271 while vi < NX_LLM_N_VERDICTS { 272 if nx_llm_verdict_is_valid(vi) != 1 { return 30 + vi } 273 vi = vi + 1 274 } 275 276 return 0 277}