Skip to content

Reduce Zig ReleaseFast runtime codegen graph - #450

Open
ajroetker wants to merge 111 commits into
mainfrom
codex/diagnose-zig-arm64-bad-alloc
Open

Reduce Zig ReleaseFast runtime codegen graph#450
ajroetker wants to merge 111 commits into
mainfrom
codex/diagnose-zig-arm64-bad-alloc

Conversation

@ajroetker

@ajroetker ajroetker commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Outcome

Make ARM64-musl ReleaseFast the release target without a larger runner, swap, or -j1. Antfly still ships one executable, standalone still always includes embedded inference, and the executable plus C ABI library now build in one memory-budgeted graph.

The complete clean-cache ARM64 Linux musl archive build now takes 6m28s, down from 14m16s for the previous four-unit executable plus separately compiled CAPI kernel.

Production design

The executable links four independently code-generated units behind hidden C ABIs:

  • api_kernel: public API request/handler machinery
  • cli: remote CLI and HA administration
  • antfly-storage-kernel: data, metadata, serverless, standalone, Lite, and the public opaque CAPI implementation
  • inference: inference commands and standalone's embedded inference host

Data, metadata, serverless, standalone, and CAPI deliberately share the PIC storage-kernel archive because their storage graphs almost completely overlap. The executable, libantfly, and libantfly_zig_capi all consume that exact compiled archive. The shared-library link uses a private anchor and private trap-only resolutions for executable-only API/inference references, so those unrelated archives are not retained and their symbols are not exported.

Standalone/focused CAPI builds retain a dedicated capi_root.zig facade and standalone PIC object. The existing capi/root.zig public module is unchanged.

The packaging script patches Zig 0.16's build runner for aggregate max_rss accounting and supplies a 20,971,520,000-byte scheduling budget. API + CLI + storage kernel form the initial 19 GiB claim group; as the small units finish, inference can overlap the kernel within the same budget.

Measured result

Fresh isolated caches, pinned Zig 0.16, ARM64 Linux musl, ReleaseFast, stripped, baseline CPU, no -j1:

Complete archive graph Wall time Compiler units Result
Previous executable + separate CAPI 14m16s API 4 GB; CLI 2 GB; distributed 10 GB; inference 6 GB; CAPI 6 GB 32/32 passed
Shared PIC storage kernel 6m28s API 2m/2 GB; CLI 1m/1 GB; kernel 6m/5 GB; inference 3m/3 GB 30/30 passed

The final CAPI link took 1s and 224 MB RSS. The build completed successfully; the surrounding macOS /usr/bin/time returned nonzero only because its post-build sysctl kern.clockrate probe is sandbox-denied.

Artifact tradeoff:

  • executable: 60,424,496 -> 61,090,808 bytes (+666,312; 1.1%)
  • libantfly.so: 16,554,992 -> 28,297,616 bytes (+11,742,624; 70.9%)

The 55 MB form that linked API and inference into the shared library was explicitly rejected. The accepted 27 MB library exports the public antfly_db_* ABI but not runtime, API-kernel, inference-host, or link-anchor symbols.

Compiler-graph reduction

The original five authoritative compiler graphs contained 2,147 repository-file instances but only 1,204 unique files: 943 duplicate analyzed/code-generated instances. Storage accounted for 255 duplicate instances.

Reusing the distributed/storage archive removes the separate CAPI compiler graph:

  • duplicate repository-file instances: 943 -> 553 (-390; 41.4%)
  • duplicate storage-file instances: 255 -> 109 (-146; 57.3%)

This achieves the dominant ownership/codegen win at a compiled artifact boundary using the existing opaque runtime and CAPI ABIs. Data, metadata, and standalone intentionally remain co-located inside that kernel, so a mechanical rewrite of every internal DB use into wire callbacks is no longer required for the release critical path. Finer opaque handles remain useful only if those roles need to be split into separate compiler units later.

Failure diagnosis

The evidence does not support ordinary runner OOM: the original std::bad_alloc occurred with zero cgroup OOM events and only about 18–20 GB peak cgroup memory, while a direct compiler replay without Zig's --listen=- compiler-server protocol succeeded. The practical fix combines a smaller number of independently optimized graphs with corrected Zig 0.16 build-runner max_rss admission accounting.

Verification

  • fresh ARM64 Linux musl ReleaseFast executable + C ABI graph: 30/30 steps passed
  • ARM64 musl C consumer ran successfully under Alpine 3.22
  • native linked Debug graph: 36/36 steps passed
  • CAPI tests: 10 passed, 0 failed/leaked
  • top-level CLI tests: 5 passed, 0 failed/leaked
  • focused/unlinked CAPI graph with isolated caches: 17/17 steps passed
  • graph analyzer and build-runner patch tests: 11 passed
  • graph runtime/codegen boundary checks passed
  • CI verifies antfly_db_open is exported and private executable ABI symbols are absent
  • formatting, shell syntax, YAML parsing, and diff checks passed

@ajroetker

Copy link
Copy Markdown
Contributor Author

Diagnostic attempt 1 findings:

  • The baseline reproduced the same std::bad_alloc with zero cgroup OOM events.
  • Replaying the extracted compiler command under GDB without --listen=- exited normally, so it did not preserve the failing compiler mode and yielded no stack.
  • The identical command with -fno-llvm grew to about 26.6 GiB RSS within seconds. Kubernetes evicted the 32 GB node at 42 MiB available; the runner exited 137. The non-LLVM backend is therefore not viable for this compile.

The second commit changes the trace to wrap the first failing build-exe child inside the Zig build runner, preserving --listen=- while routing GDB output to an artifact.

@ajroetker

Copy link
Copy Markdown
Contributor Author

Diagnostic update from run 31054869045:

  • The normal baseline failed with std::bad_alloc at a cgroup peak of 17.6 GB and zero cgroup OOM events.
  • An uninstrumented replay preserved the full build-runner/compiler --listen=- protocol, copied the post-failure local cache, and used a brand-new private global cache. It still failed, this time as LLVM ERROR: out of memory / Allocation failed, with zero cgroup OOM events.
  • Therefore the shared /mnt/cache/zig/global cache is not required to reproduce the failure. Cache state can still affect the path/symptom, so this does not yet distinguish a server-mode cache interaction from the protocol itself.
  • The compiler was about 15.4 GB RSS when it aborted. The later 28.2 GB cgroup peak was diagnostic-only overhead while Linux wrote the roughly 15 GB core alongside the compiler memory.
  • The postmortem core confirmed SIGABRT, but the distributed Zig executable did not provide a usable symbolic unwind (?? frames / corrupt stack), so it did not identify the allocation call site.

The clean discriminator remains two independent cache clones from one identical starting state: build-runner + --listen=- on one, direct compiler without --listen=- on the other, with neither run allowed to warm the other.

@ajroetker

Copy link
Copy Markdown
Contributor Author

Cache-isolated A/B result from run 31057090590:

  • Fresh release baseline failed again: LLVM ERROR: out of memory / Buffer allocation failed.
  • Kernel policy was vm.overcommit_memory=1; cgroup memory.max=max; swap was unused; all OOM counters remained zero. The displayed 16 GB CommitLimit was not enforced (Committed_AS exceeded it during compilation).
  • Starting from the failed local-cache state, the script created two independent copies before either replay ran and gave each an empty private global cache.
  • Full build-runner/compiler-server replay with --listen=- succeeded: 9:24, 15,986,108 KiB max RSS, zero swaps.
  • Direct compiler replay without --listen=- also succeeded: 9:28, 15,869,088 KiB max RSS, zero swaps.

Conclusion: --listen=- alone is not sufficient to trigger the failure, shared global-cache corruption is not required, and adding RAM/swap is unsupported by the evidence. The initial clean release compile fails, while a replay reusing the partially populated local cache can succeed at essentially the same memory footprint. This supports a narrowly scoped one-time retry for recognized Zig/LLVM allocation failures as a release mitigation, while retaining this PR as evidence for an upstream Zig/LLVM investigation.

The diagnostic check is red only because it asserted the previously expected server-fails/direct-succeeds split; both replays actually exited 0.

@ajroetker ajroetker changed the title Diagnose Zig ARM64 ReleaseSmall bad_alloc Diagnose Zig ARM64 ReleaseFast bad_alloc Aug 6, 2026
…m64-bad-alloc

# Conflicts:
#	zig/build.zig
#	zig/pkg/antfly/src/api/table_reads.zig
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants