Skip to content

Commit 38d5dd0

Browse files
committed
Retain prompt-end checkpoints per session using existing entry limit
1 parent 698df45 commit 38d5dd0

15 files changed

Lines changed: 441 additions & 124 deletions

File tree

‎examples/llm_server/cpp/module_worker_main.cpp‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ DEFINE_int32(max_sessions, 4, "Maximum logical serving sessions");
3030
DEFINE_int32(
3131
prefix_cache_entries,
3232
0,
33-
"Retained prefix snapshots; zero disables prefix caching");
33+
"Retained snapshots: shared by default (0), per session with prompt-end checkpoints (8)");
3434
DEFINE_int32(
3535
prefix_checkpoint_interval,
3636
0,
@@ -42,7 +42,7 @@ DEFINE_int32(
4242
DEFINE_bool(
4343
prefix_checkpoint_prompt_end,
4444
false,
45-
"Best-effort retention of each request's prompt end in the bounded prefix cache");
45+
"Retain prompt-end checkpoints separately for each named session");
4646
DEFINE_int32(max_session_tokens, 2048, "Maximum retained tokens per session");
4747
DEFINE_int32(max_decode_sequences, 2, "Decode slots in one packed forward");
4848
DEFINE_int32(
@@ -52,11 +52,16 @@ DEFINE_int32(
5252

5353
int main(int argc, char** argv) {
5454
gflags::ParseCommandLineFlags(&argc, &argv, true);
55+
if (FLAGS_prefix_checkpoint_prompt_end &&
56+
gflags::GetCommandLineFlagInfoOrDie("prefix_cache_entries").is_default) {
57+
FLAGS_prefix_cache_entries = 8;
58+
}
5559
if (FLAGS_prefix_checkpoint_prompt_end && FLAGS_prefix_checkpoint_interval) {
5660
std::cerr << "--prefix_checkpoint_prompt_end cannot be combined with "
5761
"--prefix_checkpoint_interval\n";
5862
return 1;
5963
}
64+
6065
if (FLAGS_pte.empty() || FLAGS_tokenizer.empty() || FLAGS_max_sessions <= 0 ||
6166
FLAGS_max_session_tokens <= 0 || FLAGS_max_decode_sequences <= 0 ||
6267
FLAGS_max_inflight_requests <= 0 || FLAGS_prefix_cache_entries < 0 ||
@@ -79,10 +84,9 @@ int main(int argc, char** argv) {
7984
config.max_context_length = FLAGS_max_session_tokens;
8085
config.max_requests = FLAGS_max_inflight_requests;
8186
config.max_pending_operations = FLAGS_max_inflight_requests;
82-
const auto physical_sessions = static_cast<std::int64_t>(FLAGS_max_sessions) +
83-
FLAGS_prefix_cache_entries +
84-
static_cast<int64_t>(config.resolved_prefix_captures());
85-
if (physical_sessions > std::numeric_limits<int>::max()) {
87+
const auto physical_sessions = config.required_executor_rows();
88+
if (!physical_sessions ||
89+
*physical_sessions > std::numeric_limits<int>::max()) {
8690
std::cerr
8791
<< "session and prefix-cache capacity exceeds executor row limit\n";
8892
return 1;
@@ -113,7 +117,7 @@ int main(int argc, char** argv) {
113117
const auto eos = llm::get_eos_ids(tokenizer.get(), module.get());
114118
auto executor = llm::batching::ModuleExecutor::create(
115119
std::move(module),
116-
static_cast<int>(physical_sessions),
120+
static_cast<int>(*physical_sessions),
117121
FLAGS_max_session_tokens,
118122
static_cast<int>(*dtype),
119123
-1,

‎examples/llm_server/python/README.md‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,23 @@ limit; choose a value supported by the export. Capacity defaults are
6666
`--prefix-cache-entries 0`. With prefix caching enabled, `--prefix-capture-slots 0`
6767
(the default) automatically allows one pending prompt capture per admitted request,
6868
bounded by `min(max_sessions, max_inflight_requests)`. A positive value overrides
69-
this allowance. Add `--prefix-checkpoint-prompt-end` to retain later request boundaries
70-
as well as initial prompts; it requires `--prefix-cache-entries > 0`. This mode
71-
keeps the same concurrent capture allowance and skips already-retained prompt ends.
72-
The snapshot becomes reusable and its capture slot is released when the clone is
73-
collected, without waiting for the source request to finish decoding.
69+
this allowance. Alternatively, `--prefix-checkpoint-prompt-end` retains initial,
70+
continuation, and replacement prompt boundaries separately for each named session.
71+
`--prefix-cache-entries` defaults to eight snapshots per session; duplicates
72+
are skipped and LRU eviction stays within the owning session. Close/reset clears
73+
that session's snapshots. Anonymous requests neither use nor retain them.
74+
This mode reserves `max_sessions * prefix_cache_entries` snapshot rows,
75+
plus working sessions and capture slots. Eight sessions with eight checkpoints
76+
and automatic capture sizing need 80 rows in total. Capture slots are released
77+
after clone collection, without waiting for decoding to finish.
7478
Capture is best effort: occupied transient rows can prevent a snapshot.
7579
With periodic checkpoints enabled, automatic sizing uses at most two rows:
7680
checkpoint collection is serialized, but one replacement can overlap. Explicit
7781
values above two add unused row capacity in that mode.
7882
`--prefix-checkpoint-prompt-end` cannot be combined with a nonzero
7983
`--prefix-checkpoint-interval`; the launcher and native worker reject that combination.
84+
Here `--prefix-cache-entries` sets the per-session limit and defaults to eight;
85+
without prompt-end mode it keeps its shared-cache meaning and defaults to zero.
8086
Logical sessions, packed decode width, and in-flight request capacity are independent
8187
limits. Python concurrency follows advertised request capacity, not the single
8288
native delivery thread. Python reads native async streams directly through bounded

‎examples/llm_server/python/serve.py‎

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ def _parse_preflight_timeout(value):
4141

4242

4343
def _validate_limits(
44-
parser, args, *, prefix_capture_slots=0, prefix_checkpoint_prompt_end=False
44+
parser,
45+
args,
46+
*,
47+
prefix_capture_slots=0,
48+
prefix_checkpoint_prompt_end=False,
4549
) -> int:
4650
for flag, value in (
4751
("--max-context", args.max_context),
@@ -60,15 +64,12 @@ def _validate_limits(
6064
"--prefix-checkpoint-prompt-end cannot be combined with "
6165
"--prefix-checkpoint-interval"
6266
)
63-
if (
64-
args.prefix_checkpoint_interval or prefix_checkpoint_prompt_end
65-
) and not args.prefix_cache_entries:
66-
flag = (
67-
"--prefix-checkpoint-prompt-end"
68-
if prefix_checkpoint_prompt_end
69-
else "--prefix-checkpoint-interval"
67+
if prefix_checkpoint_prompt_end and not args.prefix_cache_entries:
68+
parser.error(
69+
"--prefix-checkpoint-prompt-end requires --prefix-cache-entries > 0"
7070
)
71-
parser.error(f"{flag} requires --prefix-cache-entries > 0")
71+
if args.prefix_checkpoint_interval and not args.prefix_cache_entries:
72+
parser.error("--prefix-checkpoint-interval requires --prefix-cache-entries > 0")
7273
if not 0 <= prefix_capture_slots <= _MAX_INT32:
7374
parser.error(f"--prefix-capture-slots must be between 0 and {_MAX_INT32}")
7475
# Mirrors ServingRuntimeConfig::resolved_prefix_captures for row validation.
@@ -78,9 +79,12 @@ def _validate_limits(
7879
prefix_capture_slots = min(prefix_capture_slots, 2)
7980
if not args.prefix_cache_entries:
8081
prefix_capture_slots = 0
81-
physical_sessions = (
82-
args.max_sessions + args.prefix_cache_entries + prefix_capture_slots
82+
snapshots = (
83+
args.max_sessions * args.prefix_cache_entries
84+
if prefix_checkpoint_prompt_end
85+
else args.prefix_cache_entries
8386
)
87+
physical_sessions = args.max_sessions + snapshots + prefix_capture_slots
8488
if physical_sessions > _MAX_INT32:
8589
parser.error("session and prefix-cache capacity exceeds the row limit")
8690
if not 1 <= args.port <= 65535:
@@ -118,11 +122,15 @@ def _parse_args(argv=None):
118122
parser.add_argument("--max-sessions", type=int, default=16)
119123
parser.add_argument("--max-decode-sequences", type=int, default=8)
120124
parser.add_argument("--max-inflight-requests", type=int, default=64)
121-
parser.add_argument("--prefix-cache-entries", type=int, default=0)
125+
parser.add_argument(
126+
"--prefix-cache-entries",
127+
type=int,
128+
help="Retained snapshots: shared by default (0), per named session with prompt-end checkpoints (8)",
129+
)
122130
parser.add_argument(
123131
"--prefix-checkpoint-prompt-end",
124132
action="store_true",
125-
help="Best-effort retention of each request's prompt end in the bounded prefix cache",
133+
help="Retain prompt-end checkpoints separately for each named session",
126134
)
127135
parser.add_argument(
128136
"--prefix-capture-slots",
@@ -148,6 +156,8 @@ def _parse_args(argv=None):
148156
parser.add_argument("--host", default="127.0.0.1")
149157
parser.add_argument("--port", type=int, default=8000)
150158
args = parser.parse_args(argv)
159+
if args.prefix_cache_entries is None:
160+
args.prefix_cache_entries = 8 if args.prefix_checkpoint_prompt_end else 0
151161

152162
_validate_limits(
153163
parser,

‎examples/llm_server/python/tests/test_serve_launcher.py‎

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,24 +157,38 @@ def test_checkpoint_options_reach_worker(argv, launch):
157157
assert command[command.index("--prefix_checkpoint_interval") + 1] == "512"
158158

159159

160-
def test_prompt_end_capture_reaches_worker_and_requires_cache(argv, launch):
160+
@pytest.mark.parametrize("capacity", [8, 2])
161+
def test_prompt_end_capture_reaches_worker_with_session_capacity(
162+
argv, launch, capacity
163+
):
161164
assert not serve._parse_args(argv).prefix_checkpoint_prompt_end
162-
with pytest.raises(SystemExit):
163-
serve._parse_args(argv + ["--prefix-checkpoint-prompt-end"])
164-
serve.main(argv + ["--prefix-cache-entries", "3", "--prefix-checkpoint-prompt-end"])
165+
assert (
166+
serve._parse_args(
167+
argv + ["--prefix-checkpoint-prompt-end"]
168+
).prefix_cache_entries
169+
== 8
170+
)
171+
serve.main(
172+
argv
173+
+ [
174+
"--prefix-checkpoint-prompt-end",
175+
"--prefix-cache-entries",
176+
str(capacity),
177+
]
178+
)
165179
assert (
166180
"--prefix_checkpoint_prompt_end=true"
167181
in launch.stages["spawn"].call_args.args[0]
168182
)
183+
command = launch.stages["spawn"].call_args.args[0]
184+
assert command[command.index("--prefix_cache_entries") + 1] == str(capacity)
169185

170186

171187
def test_prompt_end_rejects_periodic_capture(argv, capsys):
172188
with pytest.raises(SystemExit):
173189
serve._parse_args(
174190
argv
175191
+ [
176-
"--prefix-cache-entries",
177-
"3",
178192
"--prefix-checkpoint-prompt-end",
179193
"--prefix-checkpoint-interval",
180194
"64",
@@ -183,6 +197,42 @@ def test_prompt_end_rejects_periodic_capture(argv, capsys):
183197
assert "cannot be combined" in capsys.readouterr().err
184198

185199

200+
@pytest.mark.parametrize(
201+
"options",
202+
[
203+
["--prefix-cache-entries", "0"],
204+
["--prefix-cache-entries", "-1"],
205+
["--max-sessions", str(1 << 28)],
206+
["--prefix-cache-entries", str((1 << 31) - 1)],
207+
],
208+
)
209+
def test_session_checkpoint_limits_reject_invalid_combinations_and_row_overflow(
210+
argv, options
211+
):
212+
with pytest.raises(SystemExit):
213+
serve._parse_args(argv + ["--prefix-checkpoint-prompt-end"] + options)
214+
215+
216+
def test_session_checkpoint_rows_include_every_sessions_budget(argv):
217+
maximum = (1 << 31) - 1
218+
# Two working rows + two checkpoint budgets + one transient row.
219+
capacity = (maximum - 3) // 2
220+
flags = [
221+
"--prefix-checkpoint-prompt-end",
222+
"--max-sessions",
223+
"2",
224+
"--prefix-capture-slots",
225+
"1",
226+
"--prefix-cache-entries",
227+
]
228+
assert (
229+
serve._parse_args(argv + flags + [str(capacity)]).prefix_cache_entries
230+
== capacity
231+
)
232+
with pytest.raises(SystemExit):
233+
serve._parse_args(argv + flags + [str(capacity + 1)])
234+
235+
186236
@pytest.mark.parametrize(
187237
"sessions,requests,interval,override,expected,prompt_end",
188238
[

‎examples/llm_server/spec/README.md‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -392,15 +392,21 @@ sampled requests. With `--prefix_capture_slots=C`, it adds `N + C` physical rows
392392
for retained snapshots and transient captures; logical `--max_sessions` remains
393393
unchanged. The default `--prefix_capture_slots=0` resolves `C` automatically to
394394
`min(max_sessions, max_inflight_requests)`, so overlapping requests can each capture
395-
their initial prompt. `--prefix_checkpoint_prompt_end=true` additionally requests
396-
continuation and replacement snapshots with the same concurrent allowance.
397-
It requires nonzero cache capacity and skips exact prompt ends already retained.
398-
Snapshots are published and their transient slots released after clone collection,
399-
without waiting for the request to finish decoding.
395+
their initial prompt. Alternatively, `--prefix_checkpoint_prompt_end=true` retains
396+
initial, continuation, and replacement snapshots separately for each named session.
397+
`--prefix_cache_entries=8` is the default per-session limit. This mode adds
398+
`max_sessions * prefix_cache_entries + C` physical rows. The existing
399+
`--prefix_cache_entries` flag now sets the per-session limit when prompt-end mode
400+
is enabled; otherwise it keeps the shared-cache meaning and default of zero.
401+
Exact prompt identities already retained
402+
within the session are not cloned again. Close/reset clears the session's snapshots,
403+
and anonymous requests neither reuse nor retain them. LRU eviction stays within
404+
each session. Transient slots are released after clone collection without waiting
405+
for the request to finish decoding.
400406
With periodic checkpoints enabled, automatic sizing uses at most two
401407
rows because checkpoint collection remains serialized and one replacement can
402408
overlap. A positive value overrides automatic sizing; values above two add unused
403-
row capacity in checkpoint mode. Disabled prefix caching reserves no capture rows.
409+
row capacity in checkpoint mode. Disabling both cache modes reserves no capture rows.
404410
Capture is best effort. Prompt-end capture cannot be combined with a nonzero
405411
checkpoint interval or a custom checkpoint policy; invalid combinations are
406412
rejected. A C++ `prefix_checkpoint_policy` overrides the interval. Periodic/custom

‎examples/llm_server/spec/batching.md‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,17 @@ speculative drafting/verification remain inside the runner and executor.
3535
of N and at the initial prompt end. It requires `--prefix-cache-entries > 0`.
3636
Zero (the default) disables periodic capture. With prefix caching enabled and no
3737
other capture options, only initial prompts are retained.
38-
`--prefix-checkpoint-prompt-end` also requests snapshots of continuation and replacement
39-
prompt ends. It preserves per-request capture concurrency and cannot be combined
40-
with a nonzero interval or custom checkpoint policy. Snapshots become reusable and
41-
capture slots are released when their clones are collected, before decode completes.
38+
`--prefix-checkpoint-prompt-end` instead retains prompt ends within each named
39+
session, including continuations and replacements. `--prefix-cache-entries`
40+
defaults to eight per session. Checkpoints cannot be reused or evicted by another
41+
session, and close/reset clears them. Anonymous requests do not use this cache.
42+
This mode cannot be combined with a nonzero interval or custom checkpoint policy.
43+
In this mode `--prefix-cache-entries` sets the per-session limit (default eight);
44+
otherwise it sets the shared-cache limit (default zero). It reserves `max_sessions * prefix_cache_entries`
45+
snapshot rows in addition to working sessions and transient captures.
46+
Capture slots are released after clone collection, before decode completes.
4247
Capture is best effort: occupied transient rows can skip a boundary. Already-retained
43-
prompt ends are not cloned again. Retention uses global LRU; early checkpoints are
44-
not protected.
48+
prompt ends are not cloned again. Default retention uses LRU within each cache.
4549

4650
C++ applications can customize two independent policies in
4751
[`ServingRuntimeConfig`](../../../extension/llm/serving/serving_runtime.h):

‎examples/models/muse-glimmer/BATCHING_SERVING.md‎

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -224,23 +224,29 @@ apply. Text/image prefix snapshots are opt-in via `--prefix-cache-entries`
224224

225225
## Session Behavior
226226

227-
Prefix caching is disabled unless `--prefix-cache-entries` is nonzero. When
228-
configured without other capture options, it retains initial prompts. Add
229-
`--prefix-checkpoint-prompt-end` to request snapshots of subsequent boundaries,
230-
including tool-result continuations and replacements. Repeated or branched requests
231-
can reuse those snapshots; new tool-result text still needs prefill. Exact prompt
232-
ends already in the cache are not cloned again.
233-
Snapshots become reusable and their capture slots are released after clone
234-
collection, without waiting for decoding to finish.
227+
Without prompt-end mode, nonzero `--prefix-cache-entries` enables shared prefix
228+
caching; without other capture options it retains initial prompts. `--prefix-checkpoint-prompt-end` instead
229+
retains prompt boundaries separately for each named session, including tool-result
230+
continuations and replacements. `--prefix-cache-entries` defaults to eight
231+
checkpoints per session. Repeated or branched requests using the same session ID can
232+
reuse its snapshots; other sessions and anonymous requests cannot. New tool-result
233+
text still needs prefill. Exact prompt ends already retained in that session are
234+
not cloned again. Close/reset, including idle-session eviction, clears its snapshots.
235235

236236
Prompt-end-only capture preserves per-request capture concurrency and the default
237-
transient-row budget. All snapshots share the bounded cache and global LRU, so new
238-
boundaries can evict earlier prompts. Capture is best effort: concurrent captures
237+
transient-row budget. LRU eviction stays within each session, so one session cannot
238+
evict another's checkpoints. Capture slots are released after clone collection,
239+
without waiting for decoding to finish. Capture is best effort: concurrent captures
239240
or session replacements can occupy the available rows. Measure capture overhead
240241
and reuse for your workload before enabling it.
241242

242243
`--prefix-checkpoint-prompt-end` cannot be combined with a nonzero
243-
`--prefix-checkpoint-interval`; both the launcher and native worker reject it.
244+
`--prefix-checkpoint-interval`; both the launcher and native worker reject that combination.
245+
With prompt-end mode, `--prefix-cache-entries` is the per-session budget (default eight);
246+
without it, the same option is the shared-cache budget (default zero). This mode reserves
247+
`max_sessions * prefix_cache_entries` snapshot rows plus working sessions
248+
and capture slots: eight sessions, eight checkpoints each, and eight automatic
249+
capture slots require 80 physical rows. The feature remains disabled by default.
244250
Periodic checkpoints use one serialized lane and may skip a boundary while an
245251
earlier clone is pending. Automatic transient-row sizing is capped at two in
246252
periodic mode; prompt-end capture retains the ordinary concurrency budget.
@@ -339,7 +345,8 @@ python -m executorch.examples.models.muse_glimmer.serving.serve_batching \
339345

340346
`--data-path` is required. Image artifacts read `pos_embed.bin` beside
341347
`model.pte`, or `--pos-embed-path`. Every resident session reserves `--max-context` cells of the
342-
pool: requests, `--prefix-cache-entries` snapshots, and the resolved number of
348+
pool: requests, shared `--prefix-cache-entries` snapshots or
349+
`max_sessions * prefix_cache_entries` per-session snapshots, and the resolved number of
343350
capture slots when snapshots are enabled. `--prefix-capture-slots 0` (the default)
344351
matches request concurrency, including prompt-end-only mode; periodic checkpoints
345352
cap the automatic count at two; a

0 commit comments

Comments
 (0)