Add configurable context-window with memory-safety validation - #2240
Open
alytaphoenix wants to merge 1 commit into
Open
Add configurable context-window with memory-safety validation#2240alytaphoenix wants to merge 1 commit into
alytaphoenix wants to merge 1 commit into
Conversation
Lets a placement request cap a model instance's context length below its native max. KV cache is now costed per node during placement (pipeline layer allocation and the new tensor-parallel per-rank check) so a requested context can't silently OOM a node, and the cap is plumbed through to runtime KV-cache construction. Also closes a related gap: tensor-parallel placement previously had no per-node memory validation at all (GH exo-explore#1936), so an uneven-memory cluster could accept a rank that can't hold its equal share of weights. Known limitations, called out for on-device review: - Runtime enforcement (RotatingKVCache via max_kv_size) is code-complete but unverified without mlx hardware, particularly its interaction with the existing prefix-cache trim/restore logic. - Hybrid architectures with a custom model.make_cache() (NemotronH, Qwen3-Next) get placement-time protection only; the runtime cap isn't enforced for them (pre-existing mlx_lm behavior). - Tensor-parallel validation runs on the cycle already selected by filter_cycles_by_memory, so in multi-cycle topologies it can reject a cycle containing one small node even if another all-large cycle would fit (mirrors existing pipeline-path behavior, not a regression). - Per-rank weight estimate (storage_size // world_size) ignores replicated params (embeddings/router/norms in MoE), which under-counts slightly; the KV-cache estimate's GQA overestimate partially offsets this. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rjSfwDBTkmySmfU6NgHKF
This was referenced Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
max_context_lengthoption to instance placement, letting a request cap a model's context window below its native max — while making sure a larger context can't silently OOM a node.ModelCardgainshead_dim(parsed from config, used to size KV cache per token per layer).PlaceInstance/PlaceInstanceParamsgainmax_context_length: int | None(None= use the model's nativecontext_length; never allowed to exceed it, only cap down).kv_cache_bytes_per_layer()estimates KV-cache memory (usesnum_key_value_heads * head_dimwhen known, conservatively falls back tohidden_sizeotherwise — never under-counts)._allocate_and_validate_layers) alongside layer weights. Because higher-memory nodes already get proportionally more layers via the existing largest-remainder allocation, correctly costing KV cache here means larger-context instances naturally land more KV burden on higher-memory nodes — no new redistribution logic needed._validate_tensor_parallel_memory) — this also fixes Max available combined RDMA memory needs to account for uneven devices under tensor parallelism #1936, where an uneven-memory cluster could accept a tensor-parallel rank that can't actually hold its equal share of model weights. Now checked for both weights and KV cache._largest_fitting_context_length().max_context_lengthis threaded down throughMlxBuilder→SequentialGenerator/BatchGenerator→mlx_generate/run_prefill_for_request→make_kv_cache(..., max_kv_size=...), so the cap is (where architecturally possible) enforced at runtime, not just placement time.Known limitations (flagging for on-device review — I don't have mlx hardware in this environment)
max_kv_sizeis set,make_kv_cachereturns aRotatingKVCache. This PR routes that into the existing prefix-cache path (KVPrefixCache). I have not verified that prefix-cache trim/restore stays correct once a rotating cache has wrapped past its capacity — the existing rotating-aware helpers (copy_rotating_kv_cache, etc.) look like they were built for hybrid models' internal caches, not necessarily a top-level rotating cache reaching them via this new path. Would appreciate a maintainer with real hardware checking a long conversation that exceeds the configured cap.model.make_cache()(NemotronH, Qwen3-Next) ignoremax_kv_sizeentirely — this is pre-existingmlx_lm/make_kv_cachebehavior, not something this PR changes. Placement-time memory protection still applies (the KV estimate is still validated against node memory before load), but the runtime cap isn't enforced during generation for these architectures.filter_cycles_by_memory(chosen by download-score/total-RAM before per-node validation runs). In a topology with multiple tensor-capable cycles, this can reject a selection containing one undersized node even though another all-large cycle would have fit. This mirrors existing pipeline-path behavior (not a regression this PR introduces), but is worth a follow-up if it becomes an issue in practice.storage_size // world_size) doesn't account for replicated parameters (e.g. embeddings/router/norms in MoE models aren't actually sharded), so it slightly under-counts real per-rank weight memory. The KV-cache estimate's GQA overestimate (falling back tohidden_sizewhenhead_dim/num_key_value_headsaren't both known) partially offsets this in practice, but it's an approximation, same basis as the already-shipped pipeline path.Test plan
uv run basedpyright— 0 errors (verified identical tomainbaseline for all touched files)uv run ruff check— passesuv run ruff format— applieduv run pytest src/exo/master— 61 passed (28 intest_placement_utils.py, 27 intest_placement.py, including new tests forkv_cache_bytes_per_layer, tensor-parallel per-rank validation with and without a context cap, the Max available combined RDMA memory needs to account for uneven devices under tensor parallelism #1936 regression, and context-window placement fit/reject/native-max cases)nix fmt/nix flake checknot run in this environment (ruff + basedpyright substituted)🤖 Generated with Claude Code
https://claude.ai/code/session_011rjSfwDBTkmySmfU6NgHKF