nx_budget.nx source
↩ module page · 217 lines · 8540 B
1// nx_budget.nx -- declared resource ceiling per cell.
2//
3// Per cardinal: cooperative-resource-arbitration. A cell declares its
4// hard ceiling at instantiation; allocation-beyond-budget returns
5// denial, never crashes the host. This is the structural EXCEED-axis
6// vs Docker / WSL2 / containerd silent OOM-kill: the host OS-level
7// OOM-killer cannot reach Nishi cells because cells refuse to ask for
8// more than they declared.
9//
10// Composes:
11// nx_attention_class -- foreground class can preempt background
12// budget on contention via nx_yield
13// nx_evict_journal -- every denial / forced release is logged
14// nx_homeostasis -- when used > ceiling * threshold, migration
15// signal fires (nx_tropism resolves target)
16// nx_tier -- buffer sizes (NX_BUF_*) inform sane defaults
17// across NX_TIER_MCU..NX_TIER_HPC
18//
19// V1 ships five resource kinds; further kinds (GPU compute units,
20// FPGA cells, peer-link bandwidth) are queued.
21//
22// Gap list (V1 honest perf verdict):
23// - no atomic CAS on used counters (single-threaded cell assumption)
24// - no time-windowed rate limiting (net_bps is current-instant only)
25// - no nested sub-budgets (one flat ceiling per cell)
26// - no serialization to disk (in-memory only this phase)
27//
28// genealogy_id: nishi_cardinal_2026-05-17_cooperative_resource_arbitration
29// lineage_id: substrate_budget_v1
30//
31// nx_safety_envelope:
32// intended_use: "Per-cell resource ceiling enforcement;
33// prevents host OOM by denying over-budget asks"
34// sil_target: SIL2
35// asil_target: QM
36// dal_target: DAL C
37// evidence: [denial_before_alloc, no_silent_overshoot,
38// used_le_max_invariant]
39// hazard_register: [bug-tape-budget-counter-overflow,
40// bug-tape-release-without-prior-request]
41// residual_risk: "Host kernel may still OOM if cell count
42// exceeds budget aggregate; nx_homeostasis
43// catches this at the meta-budget layer."
44// verdict: NOT_YET_EVALUATED
45
46import "nx_syscalls.nx"
47import "nx_tier.nx"
48
49// ===== Sealed enum: NxResourceKind ================================
50
51const NX_RES_RAM: nx_int = 0
52const NX_RES_VRAM: nx_int = 1
53const NX_RES_CPU: nx_int = 2 // cpu time, microseconds per tick
54const NX_RES_DISK: nx_int = 3 // disk bytes written
55const NX_RES_NET: nx_int = 4 // network bytes per second
56const NX_RES_N_KINDS: nx_int = 5
57
58func nx_res_kind_is_valid(k: nx_int) -> nx_int {
59 if k < 0 { return 0 }
60 if k >= NX_RES_N_KINDS { return 0 }
61 return 1
62}
63
64// ===== Sealed enum: NxBudgetVerdict ==============================
65
66const NX_BUDGET_OK: nx_int = 0
67const NX_BUDGET_ERR_BAD_KIND: nx_int = 1
68const NX_BUDGET_ERR_OVER: nx_int = 2 // would exceed ceiling
69const NX_BUDGET_ERR_UNDERFLOW: nx_int = 3 // release without request
70
71// ===== Struct: NxBudget ==========================================
72//
73// One ceiling + one usage counter per resource kind. cell_id is the
74// stable identifier the eviction journal and homeostasis logs join on.
75// All counters are nx_size (pointer-width) so they scale across tiers
76// without overflow at NX_TIER_HPC scale.
77
78struct NxBudget {
79 cell_id: nx_int,
80 ram_max: nx_size,
81 ram_used: nx_size,
82 vram_max: nx_size,
83 vram_used: nx_size,
84 cpu_us_max: nx_size,
85 cpu_us_used: nx_size,
86 disk_max: nx_size,
87 disk_used: nx_size,
88 net_bps_max: nx_size,
89 net_bps_used: nx_size,
90}
91
92// ===== Constructor: allocate + zero ==============================
93
94func nx_budget_new(cell_id: nx_int,
95 ram_max: nx_size,
96 vram_max: nx_size,
97 cpu_us_max: nx_size,
98 disk_max: nx_size,
99 net_bps_max: nx_size) -> *NxBudget {
100 let b: *NxBudget = (sys_mmap(88)) as *NxBudget
101 b.cell_id = cell_id
102 b.ram_max = ram_max
103 b.ram_used = 0
104 b.vram_max = vram_max
105 b.vram_used = 0
106 b.cpu_us_max = cpu_us_max
107 b.cpu_us_used = 0
108 b.disk_max = disk_max
109 b.disk_used = 0
110 b.net_bps_max = net_bps_max
111 b.net_bps_used = 0
112 return b
113}
114
115// ===== nx_budget_request ==========================================
116//
117// Returns NX_BUDGET_OK and increments used counter when grant fits.
118// Returns NX_BUDGET_ERR_OVER and leaves state untouched when grant
119// would exceed ceiling. THE CALLER MUST NOT ALLOCATE on OVER -- this
120// is the structural OOM prevention point.
121
122func nx_budget_request(b: *NxBudget, kind: nx_int, want: nx_size) -> nx_int {
123 if nx_res_kind_is_valid(kind) == 0 { return NX_BUDGET_ERR_BAD_KIND }
124 if kind == NX_RES_RAM {
125 if b.ram_used + want > b.ram_max { return NX_BUDGET_ERR_OVER }
126 b.ram_used = b.ram_used + want
127 return NX_BUDGET_OK
128 }
129 if kind == NX_RES_VRAM {
130 if b.vram_used + want > b.vram_max { return NX_BUDGET_ERR_OVER }
131 b.vram_used = b.vram_used + want
132 return NX_BUDGET_OK
133 }
134 if kind == NX_RES_CPU {
135 if b.cpu_us_used + want > b.cpu_us_max { return NX_BUDGET_ERR_OVER }
136 b.cpu_us_used = b.cpu_us_used + want
137 return NX_BUDGET_OK
138 }
139 if kind == NX_RES_DISK {
140 if b.disk_used + want > b.disk_max { return NX_BUDGET_ERR_OVER }
141 b.disk_used = b.disk_used + want
142 return NX_BUDGET_OK
143 }
144 if kind == NX_RES_NET {
145 if b.net_bps_used + want > b.net_bps_max { return NX_BUDGET_ERR_OVER }
146 b.net_bps_used = b.net_bps_used + want
147 return NX_BUDGET_OK
148 }
149 return NX_BUDGET_ERR_BAD_KIND
150}
151
152// ===== nx_budget_release ==========================================
153//
154// Decrements used counter. Underflow (release without prior request)
155// is reported as NX_BUDGET_ERR_UNDERFLOW; counter remains at 0 so the
156// invariant used >= 0 holds even when callers misbehave.
157
158func nx_budget_release(b: *NxBudget, kind: nx_int, n: nx_size) -> nx_int {
159 if nx_res_kind_is_valid(kind) == 0 { return NX_BUDGET_ERR_BAD_KIND }
160 if kind == NX_RES_RAM {
161 if n > b.ram_used { b.ram_used = 0; return NX_BUDGET_ERR_UNDERFLOW }
162 b.ram_used = b.ram_used - n
163 return NX_BUDGET_OK
164 }
165 if kind == NX_RES_VRAM {
166 if n > b.vram_used { b.vram_used = 0; return NX_BUDGET_ERR_UNDERFLOW }
167 b.vram_used = b.vram_used - n
168 return NX_BUDGET_OK
169 }
170 if kind == NX_RES_CPU {
171 if n > b.cpu_us_used { b.cpu_us_used = 0; return NX_BUDGET_ERR_UNDERFLOW }
172 b.cpu_us_used = b.cpu_us_used - n
173 return NX_BUDGET_OK
174 }
175 if kind == NX_RES_DISK {
176 if n > b.disk_used { b.disk_used = 0; return NX_BUDGET_ERR_UNDERFLOW }
177 b.disk_used = b.disk_used - n
178 return NX_BUDGET_OK
179 }
180 if kind == NX_RES_NET {
181 if n > b.net_bps_used { b.net_bps_used = 0; return NX_BUDGET_ERR_UNDERFLOW }
182 b.net_bps_used = b.net_bps_used - n
183 return NX_BUDGET_OK
184 }
185 return NX_BUDGET_ERR_BAD_KIND
186}
187
188// ===== nx_budget_remaining ========================================
189//
190// Reports headroom on the given kind. Useful for proactive yield
191// decisions by callers that want to back off before hitting the wall.
192
193func nx_budget_remaining(b: *NxBudget, kind: nx_int) -> nx_size {
194 if kind == NX_RES_RAM { return b.ram_max - b.ram_used }
195 if kind == NX_RES_VRAM { return b.vram_max - b.vram_used }
196 if kind == NX_RES_CPU { return b.cpu_us_max - b.cpu_us_used }
197 if kind == NX_RES_DISK { return b.disk_max - b.disk_used }
198 if kind == NX_RES_NET { return b.net_bps_max - b.net_bps_used }
199 return 0
200}
201
202// ===== nx_budget_pressure_q10 =====================================
203//
204// Returns used/max in Q10 (0=idle, 1024=at ceiling). Homeostasis
205// queries this; >= 921 (90%) is the canonical migration trigger.
206
207func nx_budget_pressure_q10(b: *NxBudget, kind: nx_int) -> nx_int {
208 var max: nx_size = 0
209 var used: nx_size = 0
210 if kind == NX_RES_RAM { max = b.ram_max; used = b.ram_used }
211 if kind == NX_RES_VRAM { max = b.vram_max; used = b.vram_used }
212 if kind == NX_RES_CPU { max = b.cpu_us_max; used = b.cpu_us_used }
213 if kind == NX_RES_DISK { max = b.disk_max; used = b.disk_used }
214 if kind == NX_RES_NET { max = b.net_bps_max; used = b.net_bps_used }
215 if max <= 0 { return 0 }
216 return ((used as i64) * 1024) / (max as i64)
217}