nx_mesh_autoscale.nx source
↩ module page · 220 lines · 11895 B
1// nx_mesh_autoscale.nx -- WORKER MESH F3: sovereign AUTOSCALE-TO-ZERO controller (the Modal/KServe analog).
2//
3// A GPU worker should hold VRAM only while there is work. This is the LIFECYCLE POLICY: given whether the worker is
4// warm (model in VRAM), whether there is demand (a pending/recent job), and how long it has been idle vs a TTL, it
5// decides one action -- SPAWN (warm it), SERVE (dispatch now), HOLD (do nothing), or REAP (free VRAM). The policy is
6// identical whether it runs natively on a NishiOS+VRAM host or drives a worker-host agent; the actuation (start/stop
7// the worker process) lives at the host boundary (CreateProcessW on Windows today, native on NishiOS).
8//
9// Nishi is STRUCTURALLY ahead on scale-to-zero: idle = 0 MB / 0 procs (measured) and cold start ~1s, so demand-spawn +
10// idle-reap costs almost nothing -- exactly where the serving frameworks are "Best".
11//
12// CLI: (no args) -> self-test GATE (decision table + savings + neg-controls)
13// tick <demand> <idle_sec> <ttl_sec> -> live: probe the worker for warmth, then print the DECISION (a host
14// agent calls this each cycle and actuates SPAWN/REAP)
15// tick <demand> <idle> <ttl> <a> <b> <c> <d> -> probe an explicit worker ipv4
16// Actions: 0 HOLD 1 SPAWN 2 SERVE 3 REAP. NO fake greens: the gate proves every transition + that REAP never fires
17// while warm-and-busy and SPAWN never fires while already warm. license_tier: ORIGINAL
18import "nx_syscalls.nx"
19import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
20import "nx_runtime.nx"
21import "nx_http_client.nx"
22const AM_MAGIC_7861: i64 = 7861
23const AM_MAGIC_2048: i64 = 2048
24const AM_MAGIC_2000: i64 = 2000
25const AM_MAGIC_16384: i64 = 16384
26const AM_MAGIC_16383: i64 = 16383
27
28const AM_HOLD: i64 = 0
29const AM_SPAWN: i64 = 1
30const AM_SERVE: i64 = 2
31const AM_REAP: i64 = 3
32
33func am_w(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return 0 }
34func am_wn(fd: i64, v: i64) -> i64 {
35 let bb: *u8 = sys_mmap(28)
36 var m: i64 = v
37 if m < 0 { sys_write(fd, "-" as *u8, 1); m = 0 - m }
38 let tt: *u8 = sys_mmap(28)
39 var k: i64 = 0
40 if m == 0 { tt[0] = 48 as u8; k = 1 }
41 while m > 0 { tt[k] = (48 + (m - (m/10)*10)) as u8; m = m / 10; k = k + 1 }
42 var i: i64 = 0
43 while i < k { bb[i] = tt[k-1-i]; i = i + 1 }
44 sys_write(fd, bb, k)
45 return 0
46}
47func am_p(s: *u8) -> i64 { return am_w(1, s) }
48func am_pn(v: i64) -> i64 { return am_wn(1, v) }
49func am_atoi(s: *u8) -> i64 { var v: i64 = 0; var i: i64 = 0; while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 } return v }
50func am_action_name(a: i64) -> *u8 {
51 if a == AM_HOLD { return "HOLD" as *u8 }
52 if a == AM_SPAWN { return "SPAWN" as *u8 }
53 if a == AM_SERVE { return "SERVE" as *u8 }
54 if a == AM_REAP { return "REAP" as *u8 }
55 return "?" as *u8
56}
57
58// ---- THE POLICY (pure) ----
59// warm: model in VRAM (1/0). demand: pending/recent job (1/0). idle_sec: seconds since last activity. ttl_sec: reap threshold.
60func am_decide(warm: i64, demand: i64, idle_sec: i64, ttl_sec: i64) -> i64 {
61 if demand == 1 {
62 if warm == 0 { return AM_SPAWN } // work waiting + cold -> warm it
63 return AM_SERVE // work waiting + warm -> dispatch now
64 }
65 // no demand
66 if warm == 1 {
67 if idle_sec >= ttl_sec { return AM_REAP } // warm but idle past TTL -> free VRAM (scale to zero)
68 return AM_HOLD // warm + recently used -> keep warm briefly
69 }
70 return AM_HOLD // already cold + no demand -> at zero, nothing to do
71}
72
73// VRAM-seconds-saved vs always-on, in permil: (window - warm) / window.
74func am_savings_permil(warm_sec: i64, window_sec: i64) -> i64 {
75 if window_sec <= 0 { return 0 }
76 var off: i64 = window_sec - warm_sec
77 if off < 0 { off = 0 }
78 return (off * 1000) / window_sec
79}
80
81// ---- live warmth probe: BOUNDED connect (non-blocking + poll) so a COLD worker is detected in ~1s, never hangs.
82// A blocking connect to a firewalled/dropped port hangs for the OS default (tens of seconds) -- fatal for an
83// autoscaler that must SPAWN promptly. Non-blocking connect + poll(POLLOUT, 1000ms) bounds it. ----
84func am_resp_2xx(buf: *u8, n: i64) -> i64 {
85 var i: i64 = 0
86 while i + 3 < n {
87 if buf[i] == (50 as u8) { if buf[i+1] == (48 as u8) { if buf[i+2] == (48 as u8) { return 1 } } }
88 if buf[i] == (10 as u8) { i = n }
89 i = i + 1
90 }
91 return 0
92}
93// build a struct pollfd {int fd; short events; short revents} at pfd (8 bytes)
94func am_pollfd(pfd: *u8, fd: i64, events: i64) -> i64 {
95 pfd[0] = (fd & 0xff) as u8; pfd[1] = ((fd >> 8) & 0xff) as u8; pfd[2] = ((fd >> 16) & 0xff) as u8; pfd[3] = ((fd >> 24) & 0xff) as u8
96 pfd[4] = (events & 0xff) as u8; pfd[5] = ((events >> 8) & 0xff) as u8
97 pfd[6] = 0 as u8; pfd[7] = 0 as u8
98 return 0
99}
100func am_probe_warm(a: i64, b: i64, c: i64, d: i64) -> i64 {
101 let sa: *u8 = sys_mmap(16)
102 nx_http_client_sockaddr_ipv4(sa, a, b, c, d, AM_MAGIC_7861)
103 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM | AM_MAGIC_2048, 0) // SOCK_NONBLOCK=0x800 -> connect returns immediately
104 if fd < 0 { return 0 }
105 nx_connect_bounded(fd, sa, 16, NX_CONN_DEFAULT_MS) // EINPROGRESS expected; do not block on it
106 let pfd: *u8 = sys_mmap(8)
107 am_pollfd(pfd, fd, 4) // POLLOUT
108 let pn: i64 = sys_poll(pfd, 1, 1000) // 1s connect budget
109 if pn <= 0 { sys_close(fd); return 0 } // cold worker detected FAST (no hang)
110 let rev: i64 = (pfd[6] as i64) | ((pfd[7] as i64) << 8)
111 if (rev & 8) != 0 { sys_close(fd); return 0 } // POLLERR -> connect refused
112 if (rev & 16) != 0 { sys_close(fd); return 0 } // POLLHUP -> peer gone
113 let req: *u8 = sys_mmap(512)
114 let rl: i64 = nx_http_client_build_request("/v1/models" as *u8, 10, "worker" as *u8, 6, req)
115 sys_write(fd, req, rl)
116 am_pollfd(pfd, fd, 1) // POLLIN
117 let pn2: i64 = sys_poll(pfd, 1, AM_MAGIC_2000)
118 if pn2 <= 0 { sys_close(fd); return 0 }
119 let out: *u8 = sys_mmap(AM_MAGIC_16384)
120 let got: i64 = sys_read(fd, out, AM_MAGIC_16383)
121 sys_close(fd)
122 if got <= 0 { return 0 }
123 return am_resp_2xx(out, got)
124}
125
126func am_gate() -> i64 {
127 am_p("=== nx_mesh_autoscale: sovereign autoscale-to-zero controller (Modal/KServe analog) ===\n" as *u8)
128 // decision table
129 var t1: i64 = 0
130 if am_decide(0, 1, 0, 60) == AM_SPAWN { t1 = 1 } // cold + demand -> SPAWN
131 var t2: i64 = 0
132 if am_decide(1, 1, 0, 60) == AM_SERVE { t2 = 1 } // warm + demand -> SERVE
133 var t3: i64 = 0
134 if am_decide(1, 0, 90, 60) == AM_REAP { t3 = 1 } // warm + idle>ttl -> REAP
135 var t4: i64 = 0
136 if am_decide(1, 0, 30, 60) == AM_HOLD { t4 = 1 } // warm + idle<ttl -> HOLD (no early reap)
137 var t5: i64 = 0
138 if am_decide(0, 0, 999, 60) == AM_HOLD { t5 = 1 } // cold + no demand -> HOLD (already at zero)
139 var t6: i64 = 0
140 if am_decide(0, 1, 999, 60) == AM_SPAWN { t6 = 1 } // demand beats idle: cold+demand -> SPAWN not HOLD
141 // savings metric
142 var t7: i64 = 0
143 if am_savings_permil(30, 100) == 700 { t7 = 1 } // warm 30 of 100s -> 700 permil VRAM saved
144 // neg-controls (liar-kill): REAP must NEVER fire while busy or not-idle; SPAWN never while warm
145 var neg1: i64 = 0
146 if am_decide(1, 1, 999, 60) != AM_REAP { neg1 = 1 } // warm+demand never reaps (serves)
147 var neg2: i64 = 0
148 if am_decide(1, 0, 30, 60) != AM_REAP { neg2 = 1 } // warm+not-idle never reaps
149 var neg3: i64 = 0
150 if am_decide(1, 1, 0, 60) != AM_SPAWN { neg3 = 1 } // already warm never spawns
151
152 am_p(" T1 cold+demand->SPAWN: " as *u8); am_pn(t1)
153 am_p(" | T2 warm+demand->SERVE: " as *u8); am_pn(t2)
154 am_p(" | T3 warm+idle>ttl->REAP: " as *u8); am_pn(t3)
155 am_p(" | T4 warm+idle<ttl->HOLD: " as *u8); am_pn(t4)
156 am_p(" | T5 cold+nodemand->HOLD: " as *u8); am_pn(t5)
157 am_p(" | T6 demand-beats-idle: " as *u8); am_pn(t6)
158 am_p(" | T7 savings 30/100=700permil: " as *u8); am_pn(t7)
159 am_p(" | neg1 no-reap-while-busy: " as *u8); am_pn(neg1)
160 am_p(" | neg2 no-reap-while-fresh: " as *u8); am_pn(neg2)
161 am_p(" | neg3 no-spawn-while-warm: " as *u8); am_pn(neg3); am_p("\n" as *u8)
162
163 let sfd: i64 = sys_openat_wr("knowledge/status/mesh_autoscale.tsv" as *u8, 0x1a4)
164 if sfd >= 0 {
165 am_w(sfd, "# nx_mesh_autoscale -- scale-to-zero policy: SPAWN on demand, REAP on idle-TTL, HOLD otherwise\n" as *u8)
166 am_w(sfd, "decision_table_green\t" as *u8); am_wn(sfd, t1); am_w(sfd, "\n" as *u8)
167 am_w(sfd, "savings_permil_30of100\t" as *u8); am_wn(sfd, am_savings_permil(30, 100)); am_w(sfd, "\n" as *u8)
168 sys_close(sfd)
169 }
170
171 var pass: i64 = 0
172 if t1==1 { if t2==1 { if t3==1 { if t4==1 { if t5==1 { if t6==1 { if t7==1 { if neg1==1 { if neg2==1 { if neg3==1 { pass = 1 } } } } } } } } } }
173 if pass == 1 { am_p("MESHAUTOSCALEGATE verdict=GREEN (SPAWN/SERVE/HOLD/REAP correct; savings measured; liar-killed)\n" as *u8); return 0 }
174 am_p("MESHAUTOSCALEGATE verdict=RED\n" as *u8)
175 return 1
176}
177
178func am_streq(a: *u8, b: *u8) -> i64 {
179 var i: i64 = 0
180 while b[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
181 if a[i] != (0 as u8) { return 0 }
182 return 1
183}
184
185func main(argc: i64, argv: *i64) -> i64 {
186 if argc >= 2 {
187 let cmd: *u8 = argv[1] as *u8
188 // decide <warm> <demand> <idle> <ttl> -- pure decision, no probe (deterministic)
189 if am_streq(cmd, "decide" as *u8) == 1 {
190 if argc < 6 { am_w(2, "usage: nx_mesh_autoscale decide <warm> <demand> <idle_sec> <ttl_sec>\n" as *u8); return 2 }
191 let w: i64 = am_atoi(argv[2] as *u8)
192 let dm: i64 = am_atoi(argv[3] as *u8)
193 let idl: i64 = am_atoi(argv[4] as *u8)
194 let tl: i64 = am_atoi(argv[5] as *u8)
195 let act: i64 = am_decide(w, dm, idl, tl)
196 am_p("decide warm=" as *u8); am_pn(w); am_p(" demand=" as *u8); am_pn(dm); am_p(" idle=" as *u8); am_pn(idl); am_p("s ttl=" as *u8); am_pn(tl); am_p("s -> ACTION=" as *u8); am_p(am_action_name(act)); am_p("\n" as *u8)
197 return act
198 }
199 var same: i64 = 1
200 var qi: i64 = 0
201 let t: *u8 = "tick" as *u8
202 while t[qi] != (0 as u8) { if cmd[qi] != t[qi] { same = 0 } qi = qi + 1 }
203 if cmd[qi] != (0 as u8) { same = 0 }
204 if same == 1 {
205 if argc < 5 { am_w(2, "usage: nx_mesh_autoscale tick <demand> <idle_sec> <ttl_sec> [a b c d]\n" as *u8); return 2 }
206 let demand: i64 = am_atoi(argv[2] as *u8)
207 let idle: i64 = am_atoi(argv[3] as *u8)
208 let ttl: i64 = am_atoi(argv[4] as *u8)
209 var a: i64 = 192; var b: i64 = 168; var c: i64 = 8; var d: i64 = 193
210 if argc >= 9 { a = am_atoi(argv[5] as *u8); b = am_atoi(argv[6] as *u8); c = am_atoi(argv[7] as *u8); d = am_atoi(argv[8] as *u8) }
211 let warm: i64 = am_probe_warm(a, b, c, d)
212 let act: i64 = am_decide(warm, demand, idle, ttl)
213 am_p("tick warm=" as *u8); am_pn(warm); am_p(" demand=" as *u8); am_pn(demand); am_p(" idle=" as *u8); am_pn(idle); am_p("s ttl=" as *u8); am_pn(ttl); am_p("s -> ACTION=" as *u8); am_p(am_action_name(act)); am_p("\n" as *u8)
214 return act
215 }
216 am_w(2, "usage: nx_mesh_autoscale tick <demand> <idle_sec> <ttl_sec> (no args = gate)\n" as *u8)
217 return 2
218 }
219 return am_gate()
220}