Skip to content

os: the host chain runs on macOS — base page, host memory, topology, peak RSS (#593) - #611

Merged
midagedev merged 2 commits into
mainfrom
mac-m0b
Oct 21, 2026
Merged

midagedev merged 2 commits into
mainfrom
mac-m0b

Conversation

@midagedev

Copy link
Copy Markdown
Owner

What and why

M0b of #593: the host chain now runs on macOS, not just builds there.

The causes, and the fix for each:

  • Base page. The planner refused a base page other than 4 KiB. Apple Silicon's base page is 16 KiB. The row room now counts in the host's base page:
    • os::page_bytes takes a power of two of at least 4 KiB and refuses anything else by name.
    • placement stays free of OS calls; its callers pass the page in.
  • Host memory. The room was read from /proc/meminfo. os::host_memory keeps that read on Linux. macOS reads hw.memsize, plus host_statistics64's free, inactive and purgeable pages. HostRead gains VmStats.
  • Topology. The pool read its topology from sysfs. On macOS it now reads the performance level: its cores and L2, through os::perf_levels.
  • posix_fadvise. A posix_fadvise(DONTNEED) refusal was fatal in r8file and host_lock. A platform without the call now keeps its pages and says so once, as the populate fallback does.
  • Peak RSS. ru_maxrss is KiB on Linux and bytes on macOS. os::max_rss_bytes and os::children_max_rss_bytes return bytes on both. model-hf's fixture reads use them, and the crate drops libc.
  • /proc tests. Two CCD tests read the CPU id from /proc/thread-self/stat. They are ignored on macOS by name.

Linux numbers do not move, because the page is 4096 there.

Release notes: Under the hood — the host crates' tests pass on macOS (Apple Silicon).

Code shape

  • No second copy. Every new OS read is in bloomery-os, each with a Linux body and a macOS body, or a named Unsupported. /proc/meminfo parsing moved there unchanged.
  • Model code holds only architecture facts. The only model file touched is qwen35moe/place.rs, which passes the page to row_table_tier.
  • No silent failure.
    • A page the row room cannot count in is refused.
    • A sysctl that fails, or writes a width the reader does not take, is a named error.
    • host_statistics64 names its kern_return.
    • The fadvise fallback prints once.
  • Comments state what is true now. tools/check-comments.sh rc 0.
  • No relaxed gate. Each new OS test fn holds both OS cases. No test was deleted, and no assertion was weakened. The unsafe pins carry PIN(2026-10-21):: os goes 24 → 31; model and model-hf come down by the 5 blocks that moved.
  • cargo fmt is clean. cargo clippy on the touched crates for x86_64-unknown-linux-gnu: 0 warnings. On the Mac, one warning comes from main's qdot, not from this change.

Duplication found or removed: host_lock::page_bytes's sysconf call now goes through os::page_bytes.

What I ran

  • Move proof on the box. just narrow <main>: ptx-scan --no-jit of the scan set is identical on both sides (2016 entries). No kernel moved.
    • The tool kept all 209 gates, because Cargo.lock and Cargo.toml have no gate-paths row.
    • The batch ran the host gates of every touched crate plus gate-gpu-e2e. The kernels are proven unchanged, and the changed host paths are the planner, host memory, host-lock, r8 and the pool.
  • Fixture tier. tools/gate-batch.sh --list … --tier fixture --ledger over 41 gates → DONE total=41 red=0.
    • Deferred to the real tier: 7 by tier rules, and gate-prepare-corpus, which needs an environment variable.
  • Real tier. gate-placement and gate-gpu-e2e → DONE total=2 red=0 tier=real.
  • On a Mac (M1 Pro, pinned nightly).
    • cargo test -p bloomery-model --lib: 421 passed, 0 failed, 7 ignored. That is 5 ignored before this change, plus the 2 /proc CPU-id tests, by name.
    • cargo test -p bloomery-os -p bloomery-placement -p bloomery-threads: 204 passed.
    • Every tools/check-*.sh: rc 0 (15 scripts).
  • FAIL-first.

Hardware (if the change is for a machine)

Apple M1 Pro, macOS 27, for the native tests. The gates ran on the maintainers' workstation.

🤖 Generated with Claude Code

…mance levels and peak RSS (issue #593)

- os: page_bytes (a power of two of at least 4 KiB, else refused by name),
  host_memory (Linux: /proc/meminfo moved unchanged; macOS: hw.memsize and
  host_statistics64's free, inactive and purgeable pages), perf_levels
  (macOS sysctl hw.perflevel*; Linux refuses by name), max_rss_bytes and
  children_max_rss_bytes (bytes on both).
- placement: row_room and row_table_tier take the host's base page; the
  crate reads no OS state, its callers pass the page. host_room takes the
  platform's reading; HostRead gains VmStats.
- model: host_available_read reads os::host_memory and no longer refuses a
  16 KiB page; a platform without posix_fadvise keeps its pages and says so
  once; the two CCD tests that read /proc are ignored on macOS by name.
- threads: on macOS the topology is the performance level (its cores and
  L2); Linux's sysfs path is unchanged.
- model-hf: the fixture's peak RSS reads go through os (bytes, not raw
  ru_maxrss), and the crate drops libc.

Linux numbers are unchanged: the page is 4096 there. A unit test sizes the
same rows at 4096 and 16384 and is red on a sizing that ignores the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@midagedev midagedev added the host Host crates: qdot, threads, engram, engram-lab, runtime, sampler, levers, decision, vision, refset label Oct 21, 2026
@github-actions github-actions Bot added model The CPU engine and model files: crates/model, models, hf, gguf, tokenizer, jinja placement Placement planning: crates/placement labels Oct 21, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: MUST_FIX: macOS memory is overcounted, RSS conversion hides invalid values, parser coverage is weakened, and sysctl reads are duplicated.

Must fix

1. Count inactive purgeable pages only once

crates/os/src/lib.rs:541 Purgeable pages also occupy the active/inactive queues. Inactive purgeable pages are added twice here. With no host-room override, the inflated value passes through host_available_read into placement; a need between the real and inflated room can keep tables on the host instead of the NVMe tier, causing memory pressure.
Correct the room before macOS plans depend on it. Count free and inactive pages conservatively, and update the descriptions that also list purgeable pages:

    let pages = statistics
        .free_count
        .saturating_add(statistics.inactive_count);

2. Reject invalid RSS instead of reporting zero

crates/os/src/lib.rs:653 and crates/os/src/lib.rs:662 clamp negative readings to zero. max_rss_of returns Ok(0), and the fixture prints valid-looking zero RSS. The Linux multiplication also saturates on overflow. These silent defaults reach a consumer (kind B).
Keep invalid telemetry distinguishable before more callers adopt this API. Keep the cfg attributes; replace the Linux definition, then the macOS definition:

fn rss_bytes(maxrss: libc::c_long) -> u64 {
    u64::try_from(maxrss)
        .expect("getrusage peak RSS must be nonnegative")
        .checked_mul(1024)
        .expect("getrusage peak RSS in bytes exceeds u64")
}
fn rss_bytes(maxrss: libc::c_long) -> u64 {
    u64::try_from(maxrss).expect("getrusage peak RSS must be nonnegative")
}

3. Restore the malformed-unit test cases

crates/os/src/lib.rs:1239 omits MemTotal in every malformed-value case. Parsing stops at the missing total. Replacing production line 567's strip_suffix("kB") with split_whitespace().next() accepts MB as kB, but every assertion in this test still passes. The removed mem_available_reads_the_line_in_kb test rejected that mutant; its rejection coverage is lost without a dated reason (kind B).
Restore the previous rejection coverage in the same parser move. Replace the test definition:

fn meminfo_reads_its_lines_in_kb() {
    let text = "MemTotal: 263741212 kB\nMemAvailable: 257300180 kB\n";
    assert_eq!(
        super::meminfo_bytes(text).expect("the lines read"),
        (263_741_212 * 1024, 257_300_180 * 1024)
    );
    assert!(super::meminfo_bytes("MemTotal: 1 kB\n").is_err());
    assert!(super::meminfo_bytes("MemAvailable: 1 kB\n").is_err());
    for value in ["12x kB", "12 MB", "kB"] {
        let text = format!("MemTotal: 1 kB\nMemAvailable: {value}\n");
        assert!(super::meminfo_bytes(&text).is_err());
    }
}

4. Reuse the existing sysctl reader

crates/os/src/lib.rs:668 duplicates the CString conversion, syscall and error handling in crates/os/src/lib.rs:700. The latter already accepts four-byte values. R14 requires one owner; this removes a syscall implementation without adding a helper.
Consolidate these reads before their error handling diverges. Keep the existing cfg attribute and replace the count reader:

fn sysctl_u32(name: &str) -> Result<u32, OsError> {
    let invalid = || {
        OsError::Io(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            format!("{name} is not a count"),
        ))
    };
    let value = i32::try_from(sysctl_u64(name)?).map_err(|_| invalid())?;
    u32::try_from(value).map_err(|_| invalid())
}

Non-blocking

⚠ Pin RSS units exactly: crates/os/src/lib.rs:1287 permits an extra factor of 1024: 8 MiB becomes 8 GiB and remains below a synthetic 32 GiB host's total [derived].
Catch this unit regression now with exact assertions inside the existing test:
#[cfg(target_os = "linux")] assert_eq!(super::rss_bytes(2048), 2_097_152);
#[cfg(target_os = "macos")] assert_eq!(super::rss_bytes(2048), 2048);

Checked

Duplication: both sysctl readers inspected; reuse removes a syscall body.
Silent failure: both RSS conversions followed to fixture output.
Tests: parser early return and surviving unit mutations checked statically.
Gates: parser rejection coverage lost; unsafe-count increase has a dated PIN.
Escape catalog: no C1–C4 patterns match these findings. No ledger lessons are in force.

Notes

Validation was static; no Rust tests or GPU gates were run.

🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)

Comment thread crates/os/src/lib.rs Outdated
Comment on lines +543 to +544
.saturating_add(statistics.inactive_count)
.saturating_add(statistics.purgeable_count);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inactive purgeable pages are already counted in inactive_count; adding purgeable_count again inflates placement room.

Suggested change
.saturating_add(statistics.inactive_count)
.saturating_add(statistics.purgeable_count);
.saturating_add(statistics.inactive_count);

…egative peak; one sysctl reader

Purgeable pages also sit on the active and inactive queues, so adding them
counted inactive purgeable pages twice. A negative ru_maxrss is now a named
panic, not 0. sysctl_u32 reads through sysctl_u64. The meminfo parser test
again rejects each malformed MemAvailable beside a valid MemTotal, and the
RSS test pins the unit on each platform.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the last review:

Verdict: CLEAN: All earlier findings are resolved, and the new commits introduce no proven defect.

Checked

Duplicates: 0 new definitions; the duplicate syscall body was removed. Dryer reported 0 pairs.
Complexity: crapper flagged no changed function.
Silent failure: 4 changed paths followed to placement, topology and fixture consumers; invalid RSS panics and invalid sysctl counts return errors.
Tests: accepting MB at production line 568 now fails the malformed-unit assertion; dropping or adding a factor of 1024 at lines 657 or 665 fails the exact RSS assertions. Mutation checks were static.
Gates: the unsafe-block ceiling decreases from 31 to 30 with its dated PIN; no gate is relaxed.
Escape catalog: no C1–C4 pattern matches.

Notes

Validation was static; Rust tests and GPU gates were not run. The PR does not carry the complete GPU evidence list.

🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)

@midagedev
midagedev merged commit dd55bd9 into main Oct 21, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

host Host crates: qdot, threads, engram, engram-lab, runtime, sampler, levers, decision, vision, refset model The CPU engine and model files: crates/model, models, hf, gguf, tokenizer, jinja placement Placement planning: crates/placement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant