code wiki / (root) / nx_logadd_lib.nx

nx_logadd_lib.nx source

↩ module page · 103 lines · 5160 B

1// nx_logadd_lib.nx -- FIXED-POINT LOG-DOMAIN ARITHMETIC, no floats anywhere: the primitive a linear-chain CRF's 2// forward-backward pass needs (log-sum-exp) and the probability it reads back (exp of a non-positive log-domain value). 3// Scale LA_S = 1024 units per nat (Q10). la_lse(a, b) = ln(e^a + e^b) in Q10; la_exp_neg(x), x <= 0 -> a Q10 probability. 4// The two tables are built ONCE from first principles rather than typed: e^(-1/S) in Q30 from its Taylor series (the 5// third term is already below one unit at this scale, so the constant is exact to the last bit), the powers e^(-d/S) by 6// recurrence, and softplus(d) = S*ln(1 + e^(-d/S)) through the estate's ONE integer logarithm (nx_intlog ilog2_1024) -- 7// composed, never re-implemented. The horizon LA past which softplus rounds to zero is DERIVED at init by scanning the 8// table, never declared. Minus infinity is LA_NEG_INF, the same sentinel the aspect model's Viterbi already uses, so a 9// masked transition reads identically in both decoders. license_tier: ORIGINAL No hw writes (Rule 26). LIB. 10import "nx_syscalls.nx" 11import "nx_intlog.nx" 12 13const LA_S: i64 = 1024 // Q10: units per nat 14const LA_Q30: i64 = 1073741824 // 2^30, the exp table's unit 15const LA_Q30_SHIFT: i64 = 30 16const LA_Q30_HALF: i64 = 536870912 // rounding half for a Q30 product 17const LA_Q20_SHIFT: i64 = 20 18const LA_Q20_HALF: i64 = 524288 19const LA_LN2_Q20: i64 = 726817 // ln 2 in Q20 (0.6931471806 * 2^20 = 726,817.0), turns a Q10 log2 into a Q10 ln 20const LA_LOG2_Q30_Q10: i64 = 30720 // ilog2_1024(2^30) = 30 * 1024: subtracted so ln(1+p) reads from a Q30 (1+p) 21const LA_NEG_INF: i64 = 0 - 1000000000 // log-domain minus infinity (== SP_NEG in nx_absa_seq) 22const LA_NEG_INF_GUARD: i64 = 0 - 500000000 // anything at or below this is treated as minus infinity (an -inf plus a finite) 23const LA_TABLE_CAP: i64 = 16384 // table entries; the derived horizon is ~S*ln(2S), a little under half of this 24const LA_I64_BYTES: i64 = 8 25const LA_TAYLOR_TERMS: i64 = 6 // terms of e^(-1/S) at Q30: the 3rd is already < 1 unit, six is exact by any measure 26 27static la_expt: *i64 // e^(-d/S) in Q30, d = 0..LA_TABLE_CAP-1 28static la_spt: *i64 // S * ln(1 + e^(-d/S)) in Q10, d = 0..LA_TABLE_CAP-1 29static la_horizon_v: i64 // first d where la_spt[d] == 0 (derived at init) 30static la_ready: i64 31static la_clamps: i64 // la_exp_neg calls whose argument was above zero (rounding drift in a caller), announced not hidden 32 33// e^(-1/S) in Q30 by its alternating Taylor series, integer arithmetic only 34func la_exp_unit_q30() -> i64 { 35 var sum: i64 = LA_Q30 36 var term: i64 = LA_Q30 37 var k: i64 = 1 38 while k <= LA_TAYLOR_TERMS { 39 term = term / (LA_S * k) // term_k = term_(k-1) / (S*k) 40 if (k % 2) == 1 { sum = sum - term } else { sum = sum + term } 41 k = k + 1 42 } 43 return sum 44} 45// build the tables; idempotent; returns 1 46func la_init() -> i64 { 47 if la_ready == 1 { return 1 } 48 la_expt = sys_mmap(LA_TABLE_CAP * LA_I64_BYTES) as *i64 49 la_spt = sys_mmap(LA_TABLE_CAP * LA_I64_BYTES) as *i64 50 let c: i64 = la_exp_unit_q30() 51 la_expt[0] = LA_Q30 52 var d: i64 = 1 53 while d < LA_TABLE_CAP { 54 la_expt[d] = (la_expt[d - 1] * c + LA_Q30_HALF) >> LA_Q30_SHIFT 55 d = d + 1 56 } 57 // softplus(d) = S * ln(1 + e^(-d/S)) = S * ln2 * (log2(Q30 + expt[d]) - 30): the ONE integer log, then ln2 in Q20 58 d = 0 59 while d < LA_TABLE_CAP { 60 let l2: i64 = ilog2_1024(LA_Q30 + la_expt[d]) - LA_LOG2_Q30_Q10 // Q10 log2(1+p), 0..1024 61 var v: i64 = (l2 * LA_LN2_Q20 + LA_Q20_HALF) >> LA_Q20_SHIFT // Q10 ln(1+p) (S already inside the Q10) 62 if v < 0 { v = 0 } 63 la_spt[d] = v 64 d = d + 1 65 } 66 la_horizon_v = LA_TABLE_CAP 67 d = 0 68 var found: i64 = 0 69 while (d < LA_TABLE_CAP) & (found == 0) { 70 if la_spt[d] == 0 { la_horizon_v = d; found = 1 } 71 d = d + 1 72 } 73 la_clamps = 0 74 la_ready = 1 75 return 1 76} 77func la_horizon() -> i64 { return la_horizon_v } 78func la_clamp_count() -> i64 { return la_clamps } 79func la_is_neg_inf(x: i64) -> i64 { if x <= LA_NEG_INF_GUARD { return 1 } return 0 } 80// ln(e^a + e^b) in Q10 81func la_lse(a: i64, b: i64) -> i64 { 82 if la_is_neg_inf(a) == 1 { return b } 83 if la_is_neg_inf(b) == 1 { return a } 84 var m: i64 = a 85 var d: i64 = a - b 86 if b > a { m = b; d = b - a } 87 if d >= la_horizon_v { return m } 88 return m + la_spt[d] 89} 90// e^x for x <= 0, as a Q10 probability (0..LA_S); x > 0 (a caller's rounding drift) clamps to LA_S and is counted 91func la_exp_neg(x: i64) -> i64 { 92 if x > 0 { la_clamps = la_clamps + 1; return LA_S } 93 let d: i64 = 0 - x 94 if d >= LA_TABLE_CAP { return 0 } 95 return (la_expt[d] + LA_Q20_HALF) >> LA_Q20_SHIFT 96} 97// e^x for x <= 0 in Q30 (the finer reading, for callers that accumulate many small probabilities) 98func la_exp_neg_q30(x: i64) -> i64 { 99 if x > 0 { la_clamps = la_clamps + 1; return LA_Q30 } 100 let d: i64 = 0 - x 101 if d >= LA_TABLE_CAP { return 0 } 102 return la_expt[d] 103}