Coverage for src/lilbee/providers/fleet/ctx.py: 100%
42 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
1"""Context sizing for a chat model, from gguf-parser estimates.
3:func:`fit_single_ctx` sizes one device against the budget the planner scaled;
4:func:`fit_split_ctx` sizes a tensor split against the busiest card's headroom
5rather than the summed pool. Both bisect the same quantized grid.
6``engine_params.resolve_chat_ctx`` is the entry point for the single-device fit
7and holds the header-math fallback. See docs/architecture.md (VRAM estimation).
8"""
10from __future__ import annotations
12from collections.abc import Callable, Sequence
13from pathlib import Path
15from lilbee.core.config.enums import KvCacheType
16from lilbee.providers.engine_params import chat_ctx_ceiling
17from lilbee.providers.fleet.placement import _vram_proportional_split
18from lilbee.providers.fleet.vram import estimate_instance_footprint, usable_vram_fraction
19from lilbee.providers.model_cache import _DYNAMIC_CTX_FLOOR, _DYNAMIC_CTX_QUANTUM
22def _largest_fitting_ctx(upper: int, fits: Callable[[int], bool]) -> int:
23 """The largest quantized per-slot n_ctx at or below *upper* that *fits*.
25 The grid is ``_DYNAMIC_CTX_FLOOR`` plus whole ``_DYNAMIC_CTX_QUANTUM`` steps,
26 so every probe is a context the engine launches with cleanly. Returns the
27 floor when even the floor overflows, which leaves the caller to refuse a
28 window too small to serve.
29 """
30 if not fits(_DYNAMIC_CTX_FLOOR):
31 return _DYNAMIC_CTX_FLOOR
32 steps = max(0, (upper - _DYNAMIC_CTX_FLOOR) // _DYNAMIC_CTX_QUANTUM)
33 lo, hi, best = 0, steps, 0
34 while lo <= hi:
35 mid = (lo + hi) // 2
36 if fits(_DYNAMIC_CTX_FLOOR + mid * _DYNAMIC_CTX_QUANTUM):
37 best, lo = mid, mid + 1
38 else:
39 hi = mid - 1
40 return _DYNAMIC_CTX_FLOOR + best * _DYNAMIC_CTX_QUANTUM
43def fit_single_ctx(
44 model_path: Path,
45 *,
46 meta: dict[str, str] | None,
47 slots: int,
48 available_bytes: int,
49 gpu_layers: int,
50 flash_attn: bool,
51 kv_cache_type: KvCacheType,
52 unified: bool,
53 ctx_ceiling: int,
54 expert_offload: tuple[str, ...],
55) -> int:
56 """Largest quantized n_ctx whose gguf-parser estimate fits *available_bytes*.
58 The estimator prices the cache each layer of this architecture actually
59 holds, so a linear-attention, sliding-window or MLA model is granted the
60 window it can serve rather than one budgeted for dense attention everywhere.
62 *available_bytes* is the budget the caller already scaled
63 (``planning.plan_sizing_budget``), so no further fraction applies here.
64 *unified* charges the shared-memory figure, which is the whole resident
65 footprint on a host whose GPU memory is the system's memory.
66 *expert_offload* names the tensors the launch moves to system memory, so a
67 mixture-of-experts model is not charged VRAM for experts it will not hold.
68 """
69 if available_bytes <= 0:
70 return _DYNAMIC_CTX_FLOOR
71 upper = min(chat_ctx_ceiling(meta, model_path), ctx_ceiling)
73 def _fits(per_slot: int) -> bool:
74 est = estimate_instance_footprint(
75 model_path,
76 ctx=per_slot,
77 slots=slots,
78 gpu_layers=gpu_layers,
79 flash_attn=flash_attn,
80 kv_cache_type=kv_cache_type,
81 expert_offload=expert_offload,
82 )
83 return est.footprint(unified=unified) <= available_bytes
85 return _largest_fitting_ctx(upper, _fits)
88def fit_split_ctx(
89 model_path: Path,
90 *,
91 meta: dict[str, str] | None,
92 slots: int,
93 ratio: tuple[int, ...],
94 per_device_free_bytes: Sequence[int],
95 gpu_layers: int,
96 flash_attn: bool,
97 kv_cache_type: KvCacheType,
98 ctx_ceiling: int,
99 expert_offload: tuple[str, ...],
100) -> int:
101 """Largest quantized per-slot n_ctx that fits every card, capped at *ctx_ceiling*.
103 Binary-searches the gguf-parser estimate at the launch tensor-split *ratio*:
104 each probe passes a per-slot value, which the estimator charges across the
105 slot count as the server does, and accepts it when every device's own share
106 stays under that device's usable headroom. *ctx_ceiling* is the working context the
107 caller planned for (``planning._placement_estimate_ctx``: a ``cfg.num_ctx`` pin,
108 else ``cfg.chat_n_ctx_target``); the search never exceeds it, nor the model's
109 trained context. Note the ceiling bounds the PER-SLOT window, not the total:
110 placement reserves KV for one full window, while a split whose cards hold
111 several may serve up to ``_CHAT_SLOTS`` of them. What keeps that honest is
112 the per-device check below against real free bytes, not the placement
113 reserve. Falls to the floor when even the floor overflows; plan_launches
114 refuses a chat launch left at a window below the minimum grounded prompt.
116 An empty *ratio* is the tight placement, which launches without one so the
117 engine runs its own fit pass. It is also the estimator's only device-count
118 signal, so size against a headroom-proportional one here: without it
119 gguf-parser reports the whole model as a single card and nothing fits.
120 """
121 headrooms = [int(free * usable_vram_fraction()) for free in per_device_free_bytes]
122 if min(headrooms) <= 0:
123 return _DYNAMIC_CTX_FLOOR
124 if not ratio and len(per_device_free_bytes) > 1:
125 by_position = {i: float(free) for i, free in enumerate(per_device_free_bytes)}
126 ratio = _vram_proportional_split(list(by_position), by_position)
127 # Bound the per-slot search by the planned working context, not just the model's
128 # trained max: filling VRAM to that max OOM'd large tensor-split models under
129 # load (a 235B took the full 262144-token ctx and crashed). The caller passes the
130 # target placement sized its reserve against, so no single sequence exceeds the
131 # plan; the total across slots can, and is held instead by the per-device
132 # headroom test, which measures each card's real free bytes at launch.
133 upper = min(chat_ctx_ceiling(meta, model_path), ctx_ceiling)
135 def _peak_fits(per_slot: int) -> bool:
136 est = estimate_instance_footprint(
137 model_path,
138 ctx=per_slot,
139 slots=slots,
140 gpu_layers=gpu_layers,
141 flash_attn=flash_attn,
142 kv_cache_type=kv_cache_type,
143 tensor_split=ratio,
144 expert_offload=expert_offload,
145 )
146 shares = est.per_device_vram
147 if len(shares) != len(headrooms):
148 # No usable per-device breakdown: fall back to peak vs the tightest card.
149 return est.peak_footprint(unified=False) <= min(headrooms)
150 return all(share <= room for share, room in zip(shares, headrooms, strict=True))
152 return _largest_fitting_ctx(upper, _peak_fits)