Skip to content

feat(moe): per-layer host-bank residency - #112

Merged
andy-yang-1 merged 3 commits into
mainfrom
feat/split-residency
Aug 24, 2026
Merged

feat(moe): per-layer host-bank residency#112
andy-yang-1 merged 3 commits into
mainfrom
feat/split-residency

Conversation

@jason-fxz

Copy link
Copy Markdown
Collaborator

Why

#55: on WSL/Windows, WDDM caps pinned (page-locked) memory to ~50% of RAM -- MoE models whose
host bank sources exceed the cap cannot be served through the offload backend.

Workaround

Per-layer host-bank residency: keep only the GPU layers' banks pinned; the remaining layers'
banks are OS-locked (mlock, no pin quota) and decode on the CPU MoE executor.

Changes

  • --moe-cpu-layers (id list / count / fraction)
    • now accepted under --moe-backend hybrid too (was offload-only)
    • on pin-capped hosts, the selected layers' banks are locked instead of pinned
    • unset = auto: lock just enough head+tail layers to fit the budget; 0 opts out
    • plain Linux (no cap): behavior unchanged, all banks stay pinned
  • FREETOKEN_PIN_BUDGET_GB: override the pin budget (default: WSL -> 40% RAM, else uncapped)
  • --moe-backend cpu over budget: degrade to all-locked instead of dying in cudaHostRegister
  • Residency plumbing
    • FTW loader threads per-layer labels explicitly; raw loaders (safetensors/GGUF/model
      hooks) honor them ambiently via requested_residency() -- no loader signature changes
    • locked layers: prefill = synchronous whole-layer pageable copy (overlap disabled),
      copy plan skips their rows, decode routes to the CPU executor
    • mlock failure (RLIMIT_MEMLOCK): warn once, stay pageable, never abort the load
    • auto paths engage only when the CPU executor can serve the model (activation / _cpu_moe
      ABI / expert format); otherwise stay fully pinned on the GPU path

Testing

  • unit: residency validation, locked-layer materialize contract, copy-plan skip, settle routing
  • H100 e2e, raw (gpt-oss-20b mxfp4, Qwen3.6-35B-A3B NVFP4) + FTW: locked bytes == VmLck
    in every configuration; FTW vs raw identical greedy outputs for the same split

Copilot AI lite review requested due to automatic review settings August 23, 2026 21:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@andy-yang-1

Copy link
Copy Markdown
Collaborator

@jason-fxz LGTM

@andy-yang-1
andy-yang-1 merged commit e0a3bbc into main Aug 24, 2026
avlp12 pushed a commit to avlp12/FreeToken that referenced this pull request Aug 24, 2026
Brings in from FlashML-org/FreeToken main (5 commits since our merge base):

  bd372b6 fix(checkpoint): FTW truncated shard raises OSError instead of
          silent garbage load (FlashML-org#25)
  e0a3bbc Merge pull request FlashML-org#112 from FlashML-org/feat/split-residency
  831d38a fix(moe): report the residency banks actually settle at
  eebb3f5 feat(moe): estimate expert-bank bytes from the model config
  c41833b feat(moe): per-layer host-bank residency -- split lock-CPU/pin-GPU
          layers, auto selection under capped pin quota
  184a4f1 build(kernel-cache): add sm_80 (A100/A800) to default arches (FlashML-org#75)

Conflicts (all in files where upstream's split-residency work and our
own MoE/offload patches touched the same lines) were resolved to keep
both intents, not to pick a side:

* moe/host_banks.py: upstream's split-residency HostBank (backing="cuda"
  born-pinned option, LOCKED/PinPipeline/ResidencyPlan machinery) merged
  with our explicit 4096-byte alignment invariant (_off/_reg_len, aligned
  memoryview()/pin()). The alignment carve now applies to BOTH the mmap
  and the cuda backing, not just mmap. Also fixed a latent bug the
  combination would otherwise have introduced: HostBank.lock() mlock'd
  len(self._buf) (the over-allocated padded buffer) starting at the
  aligned self.addr, which overruns the mmap by up to one block; now
  mlocks self._reg_len like pin() already did.
* moe/expert_banks.py: our bank-alignment startup diagnostic
  (_log_bank_alignment/_ALIGN) and upstream's parallel-reader support
  flag (_PARALLEL_READER_SUPPORTED) are independent additions next to
  each other -- kept both. In load_expert_banks, kept upstream's
  requested_residency()/_echo_residency() wrapping and added our
  _log_bank_alignment(banks) call right after the build, before echoing
  residency back to the caller.
* models/deepseek_v4/moe.py: kept both prefill_routed guards -- our
  speculative-verify-wearing-a-prefill's-clothes check (route to
  _decode_routed under hybrid decode) runs first, then upstream's
  crossover-size-or-unpinned-layer check (route to
  super()._prefill_routed) -- both gate independently to their own
  fallback path.
* moe/offload_cache.py: in copy_missing(), reordered so the unpinned
  (LOCKED) whole-layer pageable materialize check (upstream) runs before
  the --moe-copy-engine DMA doorbell dispatch (ours): an unpinned layer
  has no device alias, so it must never reach the doorbell path, which
  assumes a pinned bank layout.
* engine/engine.py: took upstream's version of the cpu_layer_ids
  resolution -- it's a strict superset of ours (adds _auto_cpu_layers
  selection under a capped pin budget, which is the feature this sync
  exists to pull in); our `n_moe` local was just an unused-elsewhere
  alias, nothing of ours was lost.

Also (per the task): rewrote sync-upstream.sh to `git merge` instead of
`git rebase` -- uran-custom already carries its own merge commit
(pr69/pr70/pr71), and rebasing across an existing merge commit invites
history corruption.

Verified in worktree /root/ft-sync (PYTHONPATH override, not the live
editable install at /root/FreeToken):
- python3 -m compileall python/freetoken: clean
- import freetoken + the 7 touched modules: clean
- /root/test_dma_doorbell.py (eager + graph phases): PASS
- /root/test_bank_alignment.py: PASS
- /root/test_moe_copy_engine_gate.py: PASS
- /root/test_dma_dead_flag.py: PASS
- ad hoc HostBank backing="cuda" (born-pinned) + lock() mlock-length fix
  smoke test: PASS

Our uran-specific features (host_kv_tier.py, DmaCopyService + gating,
alignment invariant, --moe-collect-stats, scheduler idle hooks) are
unaffected outside the files above and behave identically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants