code wiki / (root) / nx_speculative_verify.nx

nx_speculative_verify.nx source

↩ module page · 231 lines · 9538 B

1// nx_speculative_verify.nx -- H3 speculative decoding bits-up. 2// 3// Per NISHI_ELDER_AI_OFF_DOCKER_2026_05_20.md ยง2.1 H3: small fast 4// draft model proposes K tokens; large target model verifies them 5// in one parallel forward pass; substrate keeps the longest 6// accept-prefix + 1 bonus token (the target's own next-token 7// prediction at the rejection boundary is free). Leviathan 2023 8// + Chen 2023 "Accelerating Large Language Model Decoding with 9// Speculative Sampling." Practical wins: 2-3x throughput at 10// equivalent quality when draft + target agree on common tokens. 11// 12// V1 mechanics: the substrate primitive does NOT run a model. 13// It accepts the draft tokens (what the small model proposed) 14// and a parallel accept_flag array (1 == target verified, 0 == 15// target rejected) computed upstream by the verification kernel, 16// PLUS the target's own next-token at the rejection boundary 17// (the "free" bonus token). It returns the longest accept-prefix 18// length and the count of tokens actually committed to the 19// caller's output buffer. 20// 21// Composition path: 22// - nx_speculative_session_new(K, request_id) on admit 23// - For each verification round: nx_speculative_verify(session, 24// draft_tokens, accept_flags, K_actual, target_next_token, 25// out_committed) returns count_committed 26// - Per-session stats (drafted / accepted / rounds) accumulate 27// so the consumer can monitor the acceptance rate and back 28// off speculative depth when the draft model is mispredicting 29// - Acceptance rate exposed as Q16 fixed-point (rate * 65536) 30// to keep the substrate primitive integer-only 31// 32// Pure substrate logic. No Linux features. Composes with shipped 33// nx_batch_scheduler (each batch slot can carry a NxSpeculativeSession 34// pointer for its decoding state). 35// 36// V1 honest scope: 37// - Fixed max-K (NX_SPEC_MAX_K = 16) per round 38// - Caller provides verification flags (substrate doesn't run the 39// target forward pass; that's a tensor kernel one layer up) 40// - Greedy verification only (no sampled-rejection-resampling; 41// queued for V2 when probabilistic-sampling lands) 42// - Stats are per-session only (no global aggregation; that's 43// a consumer concern) 44// 45// genealogy_id: leviathan_speculative_sampling_2023 + 46// chen_accelerating_llm_decoding_2023 + 47// cardinal_2026-05-20_elder_ai_off_docker + 48// cardinal_2026-05-20_bits_up_nishi_not_linux 49// lineage_id: substrate_speculative_verify_v1 50// 51// nx_capability_manifest: 52// variant_class: speculative_decoding 53// variant_id: speculative_verify_v1_greedy 54// requires_isa: [rv32i, rv64imac, x86_64, cortex_m, armv7a, aarch64, wasm32] 55// requires_syscalls: [mmap] 56// requires_ram_min_b: 2048 57// tier_floor: NX_TIER_INF_MOBILE 58// tier_ceiling: NX_TIER_INF_HPC 59// cost_model: 60// flops_per_n: 1.0 // O(K) verify per round 61// bytes_per_n: 16.0 // session bookkeeping 62// syscalls_per_n: 0.0 63// adversary_class: THREAT_AI_ADVERSARY 64// 65// nx_safety_envelope: 66// intended_use: "Speculative-decoding accept-prefix calculator; 67// bits-up; composes with nx_batch_scheduler" 68// sil_target: SIL2 69// evidence: [canary_bracketed, greedy_prefix_invariant, 70// stats_monotonic, bonus_token_at_boundary] 71// verdict: NOT_YET_EVALUATED 72 73import "nx_syscalls.nx" 74const NX_MAGIC_65536: i64 = 65536 75 76// ===== Constants ================================================= 77const NX_SPEC_MAX_K: i64 = 16 78 79// Verdicts. 80const NX_SPEC_OK: i64 = 0 81const NX_SPEC_BAD_INPUT: i64 = 1 82const NX_SPEC_BAD_K: i64 = 2 83const NX_SPEC_TAMPER: i64 = 3 84const NX_SPEC_N_VERDICTS: i64 = 4 85 86func nx_spec_verdict_is_valid(v: i64) -> i64 { 87 if v < 0 { return 0 } 88 if v >= NX_SPEC_N_VERDICTS { return 0 } 89 return 1 90} 91 92// Canary magic. 93const NX_SPEC_SESSION_CANARY_PRE: i64 = 0x537065635365737A // "SpecSesz" 94const NX_SPEC_SESSION_CANARY_POST: i64 = 0x53657373456E6464 // "SessEndd" 95 96// ===== Session struct ================================================= 97struct NxSpeculativeSession { 98 canary_pre: i64, 99 request_id: i64, 100 max_k: i64, // upper bound on per-round drafts; <= NX_SPEC_MAX_K 101 total_drafted: i64, // running total of draft tokens seen 102 total_accepted: i64, // running total accepted (excludes bonus) 103 total_committed: i64, // running total committed (accepted + bonus) 104 total_rounds: i64, // number of verify rounds processed 105 canary_post: i64, 106} 107 108func nx_spec_session_is_valid(s: *NxSpeculativeSession) -> i64 { 109 if (s as i64) == 0 { return 0 } 110 if s.canary_pre != NX_SPEC_SESSION_CANARY_PRE { return 0 } 111 if s.canary_post != NX_SPEC_SESSION_CANARY_POST { return 0 } 112 if s.max_k <= 0 { return 0 } 113 if s.max_k > NX_SPEC_MAX_K { return 0 } 114 if s.total_drafted < 0 { return 0 } 115 if s.total_accepted < 0 { return 0 } 116 if s.total_committed < 0 { return 0 } 117 if s.total_rounds < 0 { return 0 } 118 // Monotonic invariant: accepted <= drafted; committed = accepted + rounds (bonus per round) 119 if s.total_accepted > s.total_drafted { return 0 } 120 return 1 121} 122 123// ===== Constructor ================================================= 124func nx_speculative_session_new(request_id: i64, max_k: i64) -> *NxSpeculativeSession { 125 if max_k <= 0 { return (0 as i64) as *NxSpeculativeSession } 126 if max_k > NX_SPEC_MAX_K { return (0 as i64) as *NxSpeculativeSession } 127 128 let s: *NxSpeculativeSession = (sys_mmap(64)) as *NxSpeculativeSession 129 s.canary_pre = NX_SPEC_SESSION_CANARY_PRE 130 s.request_id = request_id 131 s.max_k = max_k 132 s.total_drafted = 0 133 s.total_accepted = 0 134 s.total_committed = 0 135 s.total_rounds = 0 136 s.canary_post = NX_SPEC_SESSION_CANARY_POST 137 return s 138} 139 140// ===== Verify ================================================= 141// Inputs: 142// s -- session 143// draft_tokens -- *i64 array of K_actual token ids the draft model produced 144// accept_flags -- *i64 array of K_actual; 1 == target verified, 0 == rejected 145// k_actual -- count of draft tokens this round (1 <= k <= session.max_k) 146// target_next -- target model's predicted next-token AT the rejection 147// boundary (or after the last-accepted draft, if all K 148// accepted). This is the "free" bonus token. 149// out_committed -- *i64 array, capacity at least max_k+1; receives the 150// committed token sequence. 151// 152// Returns: count of tokens written to out_committed. On success this is 153// (accept_prefix_len + 1). On error returns 0 - <verdict>. 154func nx_speculative_verify(s: *NxSpeculativeSession, draft_tokens: *i64, accept_flags: *i64, k_actual: i64, target_next: i64, out_committed: *i64) -> i64 { 155 if nx_spec_session_is_valid(s) != 1 { return 0 - NX_SPEC_TAMPER } 156 if k_actual <= 0 { return 0 - NX_SPEC_BAD_K } 157 if k_actual > s.max_k { return 0 - NX_SPEC_BAD_K } 158 if (draft_tokens as i64) == 0 { return 0 - NX_SPEC_BAD_INPUT } 159 if (accept_flags as i64) == 0 { return 0 - NX_SPEC_BAD_INPUT } 160 if (out_committed as i64) == 0 { return 0 - NX_SPEC_BAD_INPUT } 161 162 // Longest-accept-prefix: count consecutive 1s until first non-1. 163 var real_prefix: i64 = 0 164 var i: i64 = 0 165 var done: i64 = 0 166 while i < k_actual { 167 if done == 0 { 168 if accept_flags[i] == 1 { 169 real_prefix = real_prefix + 1 170 } else { 171 done = 1 172 } 173 } 174 i = i + 1 175 } 176 177 // Emit committed sequence: prefix tokens from draft + bonus target_next. 178 var j: i64 = 0 179 while j < real_prefix { 180 out_committed[j] = draft_tokens[j] 181 j = j + 1 182 } 183 out_committed[real_prefix] = target_next 184 185 s.total_drafted = s.total_drafted + k_actual 186 s.total_accepted = s.total_accepted + real_prefix 187 s.total_committed = s.total_committed + real_prefix + 1 188 s.total_rounds = s.total_rounds + 1 189 190 return real_prefix + 1 191} 192 193// ===== Stats accessors ================================================= 194func nx_spec_total_drafted(s: *NxSpeculativeSession) -> i64 { 195 if nx_spec_session_is_valid(s) != 1 { return 0 - 1 } 196 return s.total_drafted 197} 198 199func nx_spec_total_accepted(s: *NxSpeculativeSession) -> i64 { 200 if nx_spec_session_is_valid(s) != 1 { return 0 - 1 } 201 return s.total_accepted 202} 203 204func nx_spec_total_committed(s: *NxSpeculativeSession) -> i64 { 205 if nx_spec_session_is_valid(s) != 1 { return 0 - 1 } 206 return s.total_committed 207} 208 209func nx_spec_total_rounds(s: *NxSpeculativeSession) -> i64 { 210 if nx_spec_session_is_valid(s) != 1 { return 0 - 1 } 211 return s.total_rounds 212} 213 214// Acceptance rate as Q16 fixed-point: (total_accepted * 65536) / total_drafted. 215// Returns 0 when total_drafted == 0 (no rounds yet -> rate undefined, report 0). 216func nx_spec_acceptance_rate_q16(s: *NxSpeculativeSession) -> i64 { 217 if nx_spec_session_is_valid(s) != 1 { return 0 - 1 } 218 if s.total_drafted == 0 { return 0 } 219 let num: i64 = s.total_accepted * NX_MAGIC_65536 220 return num / s.total_drafted 221} 222 223func nx_spec_session_request_id(s: *NxSpeculativeSession) -> i64 { 224 if nx_spec_session_is_valid(s) != 1 { return 0 - 1 } 225 return s.request_id 226} 227 228func nx_spec_session_max_k(s: *NxSpeculativeSession) -> i64 { 229 if nx_spec_session_is_valid(s) != 1 { return 0 - 1 } 230 return s.max_k 231}