code wiki / _hdl_build / nx_gen_worker.nx
nx_gen_worker.nx source
↩ module page · 178 lines · 11726 B
1// nx_gen_worker.nx -- the GenWorker object: the GPU-worker CLIENT. Owns dispatch to a worker (a.b.c.d:port) for
2// image gen (/v1/images/generations) and chat (/v1/chat/completions), plus the non-blocking TCP health-check that
3// makes multi-worker availability-based selection + failover safe (a powered-off worker must never hang a dispatch).
4// Extracted from nx_gen_orchestrator (monolith -> objects); behaviour-preserving. THIS is where ER-8 model-select-by-
5// intent will land (route SFW->controlled model, explicit->turbo). Self-contained (private gw_* helpers). Object
6// model: companion_arch.md. license_tier: ORIGINAL
7import "nx_syscalls.nx"
8import "nx_netprobe_lib.nx"
9import "nx_gen_conf_lib.nx" // G7/G14 (2026-08-30): the lane's one two-path conf reader
10
11func gw_cat(dst: *u8, off: i64, s: *u8) -> i64 { var o: i64=off; var i: i64=0; while s[i]!=(0 as u8){dst[o]=s[i]; o=o+1; i=i+1} return o }
12func gw_catb(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { var o: i64=off; var i: i64=0; while i<n {dst[o]=src[i]; o=o+1; i=i+1} return o }
13func gw_itoa(dst: *u8, off: i64, v: i64) -> i64 { let t: *u8=sys_mmap(28); var m: i64=v; var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var o: i64=off; var q: i64=k-1; while q>=0{dst[o]=t[q];o=o+1;q=q-1} return o }
14
15// ONE read loop for every dispatch in this object. A read ERROR (r<0 -- which SO_RCVTIMEO delivers as
16// EAGAIN once the socket timeout expires) is NOT a clean EOF (r==0). The previous `if r <= 0` collapsed
17// them onto one branch, so the function returned 0 for BOTH, every caller tested only >0, the candidate
18// loop exhausted, and the daemon reported worker ABSENCE for a worker that was connected and rendering.
19// MEASURED 2026-08-29: a socket watch showed 192.168.8.192:7861 <- 192.168.8.227:51938 ESTABLISHED and
20// held for 164s while the caller was being told no gpu worker was available. The return space was already
21// three-valued (0-1 socket, 0-2 connect) and the caller tested one boolean.
22// >0 bytes read
23// 0 clean EOF with nothing read (peer closed without answering)
24// 0-3 TIMEOUT with nothing read -- connected and busy; the render may still be completing on the GPU
25// 0-4 PARTIAL read then timeout -- the response is truncated, which previously returned a POSITIVE
26// byte count and read as SUCCESS, resurfacing later as "worker returned no images"
27// EXTRACTED, not re-typed: the identical loop existed in go_worker_dispatch, go_worker_img2img and
28// go_chat_dispatch. A fix applied to one sibling and not the others is half a fix, and the half left
29// undone is the one that ships.
30func gw_read_full(fd: i64, respbuf: *u8, respcap: i64) -> i64 {
31 var total: i64 = 0
32 var gof: i64 = 1
33 var lastr: i64 = 0
34 while gof == 1 {
35 let r: i64 = sys_read(fd, (respbuf as i64 + total) as *u8, respcap - total)
36 if r < 0 { lastr = r; gof = 0 } else { if r == 0 { gof = 0 } else { total = total + r; if total >= respcap { gof = 0 } } }
37 }
38 if lastr < 0 { if total == 0 { return 0 - 3 } else { return 0 - 4 } }
39 return total
40}
41
42// dispatch an image-gen request: POST /v1/images/generations to worker (a.b.c.d:port), read full response.
43func go_worker_dispatch(ha: i64, hb: i64, hc: i64, hd: i64, port: i64, body: *u8, blen: i64, respbuf: *u8, respcap: i64) -> i64 {
44 let fd: i64 = sys_socket(2, 1, 0)
45 if fd < 0 { return 0 - 1 }
46 sys_set_socket_timeout(fd, 180)
47 let a: *u8 = sys_mmap(16)
48 a[0]=2 as u8; a[1]=0 as u8; a[2]=((port>>8)&0xff) as u8; a[3]=(port&0xff) as u8
49 a[4]=ha as u8; a[5]=hb as u8; a[6]=hc as u8; a[7]=hd as u8
50 var zi: i64=8; while zi<16 { a[zi]=0 as u8; zi=zi+1 }
51 if sys_connect(fd, a, 16) != 0 { sys_close(fd); return 0 - 2 }
52 let rq: *u8 = sys_mmap(16384); var o: i64 = 0
53 o = gw_cat(rq, o, "POST /v1/images/generations HTTP/1.1\r\nHost: 127.0.0.1\r\nAccept: */*\r\nConnection: close\r\nContent-Type: application/json\r\nContent-Length: " as *u8)
54 o = gw_itoa(rq, o, blen); o = gw_cat(rq, o, "\r\n\r\n" as *u8); o = gw_catb(rq, o, body, blen)
55 sys_write(fd, rq, o)
56 let total: i64 = gw_read_full(fd, respbuf, respcap)
57 sys_close(fd)
58 return total
59}
60// dispatch an IMG2IMG request: POST /sdapi/v1/img2img (A1111 shape, which stable-diffusion.cpp serves) to the worker.
61// The body carries a base64 init image (~1.4MB+ for 512px) so we write a SMALL header then STREAM the caller's body
62// from its pointer in a write-loop (no multi-MB request copy, and one sys_write cannot flush a MB in a single call).
63// Response is {"images":["<b64>"]} -> the SAME gp_response A1111 branch the txt2img path already ingests. This is
64// where structure-preserving edits (digital-twin refine, attribute change, and mask-based inpaint) dispatch.
65func go_worker_img2img(ha: i64, hb: i64, hc: i64, hd: i64, port: i64, body: *u8, blen: i64, respbuf: *u8, respcap: i64) -> i64 {
66 let fd: i64 = sys_socket(2, 1, 0)
67 if fd < 0 { return 0 - 1 }
68 sys_set_socket_timeout(fd, 200)
69 let a: *u8 = sys_mmap(16)
70 a[0]=2 as u8; a[1]=0 as u8; a[2]=((port>>8)&0xff) as u8; a[3]=(port&0xff) as u8
71 a[4]=ha as u8; a[5]=hb as u8; a[6]=hc as u8; a[7]=hd as u8
72 var zi: i64=8; while zi<16 { a[zi]=0 as u8; zi=zi+1 }
73 if sys_connect(fd, a, 16) != 0 { sys_close(fd); return 0 - 2 }
74 let hdr: *u8 = sys_mmap(1024); var ho: i64 = 0
75 ho = gw_cat(hdr, 0, "POST /sdapi/v1/img2img HTTP/1.1\r\nHost: 127.0.0.1\r\nAccept: */*\r\nConnection: close\r\nContent-Type: application/json\r\nContent-Length: " as *u8)
76 ho = gw_itoa(hdr, ho, blen); ho = gw_cat(hdr, ho, "\r\n\r\n" as *u8)
77 sys_write(fd, hdr, ho)
78 var sent: i64 = 0
79 while sent < blen { let w: i64 = sys_write(fd, (body as i64 + sent) as *u8, blen - sent); if w <= 0 { sent = blen } else { sent = sent + w } }
80 let total: i64 = gw_read_full(fd, respbuf, respcap)
81 sys_close(fd)
82 return total
83}
84// non-blocking TCP health-check ("electronic availability"): connect non-blocking + poll(POLLOUT, 2s) + SO_ERROR.
85// 1=reachable, 0=down/refused/timeout. = the multi-worker availability selector's liveness probe.
86// extracted to nx_netprobe_lib (np_probe_up) 2026-08-12 -- this was the ORIGINAL copy; nx_swarm_heal's
87// go_probe_up was the duplicate. Now BOTH import the one lib (rule 15). Thin alias keeps every call site
88// (go_worker_up) untouched = behaviour-preserving; the byte-identical probe logic now lives in one place.
89func go_worker_up(ha: i64, hb: i64, hc: i64, hd: i64, port: i64) -> i64 { return np_probe_up(ha, hb, hc, hd, port) }
90// CHAT DISPATCH BUDGET AS DATA (2026-08-30): the per-try read timeout was a literal 25 s; a measured warm completion on
91// the 5080 with a game on the card took 17.6 s, and the daemon's composed system prompt makes real turns longer, so the
92// literal was the ceiling every busy turn hit. knowledge/gen_chat_budget.conf (first integer, seconds) is read per call from
93// the daemon cwd then the nishihost tree; absent, unreadable or zero -> the historical 25, never a silent zero; a ceiling
94// bounds a typo. Same idiom as go_default_steps.
95const GW_CHAT_BUDGET_FALLBACK_S: i64 = 25
96const GW_CHAT_BUDGET_CEIL_S: i64 = 300
97func gw_chat_budget_s() -> i64 {
98 let szp: *i64 = sys_mmap(16) as *i64
99 szp[0] = 0
100 var b: *u8 = sys_read_file("knowledge/gen_chat_budget.conf" as *u8, szp)
101 if szp[0] <= 0 { b = sys_read_file("/volume1/homes/elderwesto/nishihost/knowledge/gen_chat_budget.conf" as *u8, szp) }
102 if szp[0] <= 0 { return GW_CHAT_BUDGET_FALLBACK_S }
103 var v: i64 = 0
104 var seen: i64 = 0
105 var i: i64 = 0
106 while i < szp[0] {
107 let c: i64 = b[i] & 0xff
108 if c >= 48 { if c <= 57 { v = v*10 + (c - 48); seen = 1; i = i + 1 } else { i = szp[0] } } else { if seen == 1 { i = szp[0] } else { i = i + 1 } }
109 }
110 if seen == 0 { return GW_CHAT_BUDGET_FALLBACK_S }
111 if v <= 0 { return GW_CHAT_BUDGET_FALLBACK_S }
112 if v > GW_CHAT_BUDGET_CEIL_S { return GW_CHAT_BUDGET_CEIL_S }
113 return v
114}
115// ---- G7 (2026-08-30) MASK INPAINT: the NishiLang verb over an engine surface that ALREADY answers ----------------
116// The served sd-server's /sdapi/v1/img2img decodes a base64 PNG "mask" (ONE channel: 255 = repaint, 0 = keep) and
117// honours "inpainting_mask_invert" (elder-sdcpp examples/server/main.cpp, sdapi_any2img img2img branch, read
118// 2026-08-30); with no mask it repaints the WHOLE canvas, which is the plain img2img go_handle_img2img already
119// drives. This verb APPENDS the two mask fields to an img2img JSON body the caller is composing (it emits its own
120// leading comma, so call it after an existing field) and REFUSES by named code instead of emitting a body the
121// engine would either reject or -- the silent class -- decode to an empty mask and render UNMASKED:
122// >= 0 the new offset; the body now carries "mask" and "inpainting_mask_invert"
123// GW_MASK_EMPTY no mask bytes -- a caller that meant plain img2img must not call this at all
124// GW_MASK_NOT_B64 a byte outside A-Z a-z 0-9 + / = (a data: URI prefix, whitespace, or a raw PNG byte)
125// GW_MASK_BAD_INVERT invert is neither 0 nor 1
126// The mask rides in the daemon's request window (go_serve_conn reads 131,071 B), a PRE-EXISTING bound named here
127// rather than widened here: a one-channel PNG mask of a 768x1024 photo is a few KB of base64.
128const GW_MASK_EMPTY: i64 = 0 - 1
129const GW_MASK_NOT_B64: i64 = 0 - 2
130const GW_MASK_BAD_INVERT: i64 = 0 - 3
131const GW_CH_UP_A: i64 = 65
132const GW_CH_UP_Z: i64 = 90
133const GW_CH_LO_A: i64 = 97
134const GW_CH_LO_Z: i64 = 122
135const GW_CH_D0: i64 = 48
136const GW_CH_D9: i64 = 57
137const GW_CH_PLUS: i64 = 43
138const GW_CH_SLASH: i64 = 47
139const GW_CH_EQ: i64 = 61
140func gw_b64_byte_ok(c: i64) -> i64 {
141 if c >= GW_CH_UP_A { if c <= GW_CH_UP_Z { return 1 } }
142 if c >= GW_CH_LO_A { if c <= GW_CH_LO_Z { return 1 } }
143 if c >= GW_CH_D0 { if c <= GW_CH_D9 { return 1 } }
144 if c == GW_CH_PLUS { return 1 }
145 if c == GW_CH_SLASH { return 1 }
146 if c == GW_CH_EQ { return 1 }
147 return 0
148}
149func gw_inpaint_mask(dst: *u8, off: i64, mask_b64: *u8, mlen: i64, invert: i64) -> i64 {
150 if mlen <= 0 { return GW_MASK_EMPTY }
151 if invert != 0 { if invert != 1 { return GW_MASK_BAD_INVERT } }
152 var i: i64 = 0
153 while i < mlen { if gw_b64_byte_ok(mask_b64[i] & 0xff) == 0 { return GW_MASK_NOT_B64 } i = i + 1 }
154 var o: i64 = gw_cat(dst, off, ",\"mask\":\"" as *u8)
155 o = gw_catb(dst, o, mask_b64, mlen)
156 o = gw_cat(dst, o, "\",\"inpainting_mask_invert\":" as *u8)
157 o = gw_itoa(dst, o, invert)
158 return o
159}
160// dispatch a chat request: POST /v1/chat/completions to the worker (same engine; Qwen text-encoder doubles as the
161// companion LLM). Thin authed proxy; the caller owns the messages array.
162func go_chat_dispatch(ha: i64, hb: i64, hc: i64, hd: i64, port: i64, body: *u8, blen: i64, respbuf: *u8, respcap: i64) -> i64 {
163 let fd: i64 = sys_socket(2, 1, 0)
164 if fd < 0 { return 0 - 1 }
165 sys_set_socket_timeout(fd, gw_chat_budget_s())
166 let a: *u8 = sys_mmap(16)
167 a[0]=2 as u8; a[1]=0 as u8; a[2]=((port>>8)&0xff) as u8; a[3]=(port&0xff) as u8
168 a[4]=ha as u8; a[5]=hb as u8; a[6]=hc as u8; a[7]=hd as u8
169 var zi: i64=8; while zi<16 { a[zi]=0 as u8; zi=zi+1 }
170 if sys_connect(fd, a, 16) != 0 { sys_close(fd); return 0 - 2 }
171 let rq: *u8 = sys_mmap(131072); var o: i64 = 0
172 o = gw_cat(rq, o, "POST /v1/chat/completions HTTP/1.1\r\nHost: 127.0.0.1\r\nAccept: */*\r\nConnection: close\r\nContent-Type: application/json\r\nContent-Length: " as *u8)
173 o = gw_itoa(rq, o, blen); o = gw_cat(rq, o, "\r\n\r\n" as *u8); o = gw_catb(rq, o, body, blen)
174 sys_write(fd, rq, o)
175 let total: i64 = gw_read_full(fd, respbuf, respcap)
176 sys_close(fd)
177 return total
178}