Skip to content

Commit d45a786

Browse files
committed
Add bounded opt-in capture at serving prompt boundaries
1 parent 6f6c74e commit d45a786

10 files changed

Lines changed: 235 additions & 16 deletions

File tree

‎examples/llm_server/cpp/module_worker_main.cpp‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,15 @@ DEFINE_int32(
3434
DEFINE_int32(
3535
prefix_checkpoint_interval,
3636
0,
37-
"Checkpoint interval in tokens; 0: initial prompt only. Uses global LRU.");
37+
"Periodic checkpoint interval in tokens; 0 disables periodic capture.");
3838
DEFINE_int32(
3939
prefix_capture_slots,
4040
0,
4141
"Transient capture rows; 0: size automatically from session/request limits");
42+
DEFINE_bool(
43+
prefix_capture_prompt_end,
44+
false,
45+
"Retain each request's prompt end in the bounded prefix cache");
4246
DEFINE_int32(max_session_tokens, 2048, "Maximum retained tokens per session");
4347
DEFINE_int32(max_decode_sequences, 2, "Decode slots in one packed forward");
4448
DEFINE_int32(
@@ -52,7 +56,8 @@ int main(int argc, char** argv) {
5256
FLAGS_max_session_tokens <= 0 || FLAGS_max_decode_sequences <= 0 ||
5357
FLAGS_max_inflight_requests <= 0 || FLAGS_prefix_cache_entries < 0 ||
5458
FLAGS_prefix_capture_slots < 0 || FLAGS_prefix_checkpoint_interval < 0 ||
55-
(FLAGS_prefix_checkpoint_interval > 0 &&
59+
((FLAGS_prefix_checkpoint_interval > 0 ||
60+
FLAGS_prefix_capture_prompt_end) &&
5661
FLAGS_prefix_cache_entries == 0)) {
5762
std::cerr
5863
<< "--pte and --tokenizer are required; capacity limits must be positive"
@@ -65,6 +70,7 @@ int main(int argc, char** argv) {
6570
config.prefix_cache_capacity = FLAGS_prefix_cache_entries;
6671
config.max_prefix_captures = FLAGS_prefix_capture_slots;
6772
config.prefix_checkpoint_interval = FLAGS_prefix_checkpoint_interval;
73+
config.prefix_capture_prompt_end = FLAGS_prefix_capture_prompt_end;
6874
config.max_context_length = FLAGS_max_session_tokens;
6975
config.max_requests = FLAGS_max_inflight_requests;
7076
config.max_pending_operations = FLAGS_max_inflight_requests;

‎examples/llm_server/python/serve.py‎

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
_MAX_INT32 = (1 << 31) - 1
2525

2626

27-
def _validate_limits(parser, args, *, prefix_capture_slots=0) -> int:
27+
def _validate_limits(
28+
parser, args, *, prefix_capture_slots=0, prefix_capture_prompt_end=False
29+
) -> int:
2830
for flag, value in (
2931
("--max-context", args.max_context),
3032
("--max-sessions", args.max_sessions),
@@ -37,14 +39,18 @@ def _validate_limits(parser, args, *, prefix_capture_slots=0) -> int:
3739
parser.error(f"--prefix-cache-entries must be between 0 and {_MAX_INT32}")
3840
if not 0 <= args.prefix_checkpoint_interval <= _MAX_INT32:
3941
parser.error(f"--prefix-checkpoint-interval must be between 0 and {_MAX_INT32}")
40-
if args.prefix_checkpoint_interval and not args.prefix_cache_entries:
41-
parser.error("--prefix-checkpoint-interval requires --prefix-cache-entries > 0")
42+
if (
43+
args.prefix_checkpoint_interval or prefix_capture_prompt_end
44+
) and not args.prefix_cache_entries:
45+
parser.error(
46+
"prefix checkpoints and prompt-end capture require --prefix-cache-entries > 0"
47+
)
4248
if not 0 <= prefix_capture_slots <= _MAX_INT32:
4349
parser.error(f"--prefix-capture-slots must be between 0 and {_MAX_INT32}")
4450
# Mirrors ServingRuntimeConfig::resolved_prefix_captures for row validation.
4551
if prefix_capture_slots == 0:
4652
prefix_capture_slots = min(args.max_sessions, args.max_inflight_requests)
47-
if args.prefix_checkpoint_interval:
53+
if args.prefix_checkpoint_interval or prefix_capture_prompt_end:
4854
prefix_capture_slots = min(prefix_capture_slots, 2)
4955
if not args.prefix_cache_entries:
5056
prefix_capture_slots = 0
@@ -89,6 +95,11 @@ def _parse_args(argv=None):
8995
parser.add_argument("--max-decode-sequences", type=int, default=8)
9096
parser.add_argument("--max-inflight-requests", type=int, default=64)
9197
parser.add_argument("--prefix-cache-entries", type=int, default=0)
98+
parser.add_argument(
99+
"--prefix-capture-prompt-end",
100+
action="store_true",
101+
help="Retain each request's prompt end in the bounded prefix cache",
102+
)
92103
parser.add_argument(
93104
"--prefix-capture-slots",
94105
type=int,
@@ -99,13 +110,18 @@ def _parse_args(argv=None):
99110
"--prefix-checkpoint-interval",
100111
type=int,
101112
default=0,
102-
help="Checkpoint interval in tokens; 0: initial prompt only. Uses global LRU.",
113+
help="Periodic checkpoint interval in tokens; 0 disables periodic capture.",
103114
)
104115
parser.add_argument("--host", default="127.0.0.1")
105116
parser.add_argument("--port", type=int, default=8000)
106117
args = parser.parse_args(argv)
107118

108-
_validate_limits(parser, args, prefix_capture_slots=args.prefix_capture_slots)
119+
_validate_limits(
120+
parser,
121+
args,
122+
prefix_capture_slots=args.prefix_capture_slots,
123+
prefix_capture_prompt_end=args.prefix_capture_prompt_end,
124+
)
109125

110126
args.worker_bin = os.path.expanduser(args.worker_bin)
111127
if shutil.which(args.worker_bin) is None:
@@ -177,6 +193,11 @@ async def serving_factory():
177193
if args.prefix_checkpoint_interval
178194
else []
179195
)
196+
+ (
197+
["--prefix_capture_prompt_end=true"]
198+
if args.prefix_capture_prompt_end
199+
else []
200+
)
180201
)
181202
runtime = None
182203
try:

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,40 @@ 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):
161+
assert not serve._parse_args(argv).prefix_capture_prompt_end
162+
with pytest.raises(SystemExit):
163+
serve._parse_args(argv + ["--prefix-capture-prompt-end"])
164+
serve.main(argv + ["--prefix-cache-entries", "3", "--prefix-capture-prompt-end"])
165+
assert (
166+
"--prefix_capture_prompt_end=true" in launch.stages["spawn"].call_args.args[0]
167+
)
168+
169+
170+
@pytest.mark.parametrize(
171+
"sessions,override,expected", [(8, 0, 2), (1, 0, 1), (8, 3, 3)]
172+
)
173+
def test_prompt_end_capture_bounds_automatic_rows(argv, sessions, override, expected):
174+
args = serve._parse_args(
175+
argv
176+
+ [
177+
"--prefix-cache-entries",
178+
"3",
179+
"--prefix-capture-prompt-end",
180+
"--max-sessions",
181+
str(sessions),
182+
"--prefix-capture-slots",
183+
str(override),
184+
]
185+
)
186+
assert (
187+
serve._validate_limits(
188+
Mock(), args, prefix_capture_slots=override, prefix_capture_prompt_end=True
189+
)
190+
== expected
191+
)
192+
193+
160194
@pytest.mark.parametrize(
161195
"sessions,requests,interval,override,expected",
162196
[

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,20 @@ apply. Text/image prefix snapshots are opt-in via `--prefix-cache-entries`
143143

144144
## Session Behavior
145145

146+
Prefix caching retains the initial prompt by default. Add
147+
`--prefix-capture-prompt-end` with `--prefix-cache-entries` to also retain the
148+
prompt boundary of subsequent requests, including tool-result continuations
149+
and replacements. Repeated or branched requests can then reuse those snapshots.
150+
New tool-result text still needs prefill. This is best-effort capture within the
151+
same bounded cache and transient-row limits, with global LRU retention; it can
152+
evict earlier prompts. Benchmark capture cost and memory for your workload
153+
before enabling it. It also works with `--prefix-checkpoint-interval` and adds
154+
the exact prompt end when it falls between periodic checkpoints. A pending
155+
checkpoint or another request can occupy the capture lane and skip a boundary;
156+
use prompt-end capture without periodic checkpoints when turn reuse is the
157+
priority. Automatic transient-row sizing is capped at two in either mode. The C++
158+
`prefix_checkpoint_policy`, when supplied, overrides both options.
159+
146160
- Text and image prompts use the shared prefix-reuse path. Image rows count as
147161
decoder positions; matching uses token IDs and an image-content digest with
148162
content-relative row offsets, not patch token IDs alone.

‎examples/models/muse-glimmer/runtime/runners/batching_bootstrap.cpp‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,15 @@ DEFINE_int32(
3939
DEFINE_int32(
4040
prefix_checkpoint_interval,
4141
0,
42-
"Checkpoint interval in tokens; 0: initial prompt only. Uses global LRU.");
42+
"Periodic checkpoint interval in tokens; 0 disables periodic capture.");
4343
DEFINE_int32(
4444
prefix_capture_slots,
4545
0,
4646
"Transient capture rows; 0: size automatically from session/request limits");
47+
DEFINE_bool(
48+
prefix_capture_prompt_end,
49+
false,
50+
"Retain each request's prompt end in the bounded prefix cache");
4751
DEFINE_uint64(max_image_bytes, 20 * 1024 * 1024, "Maximum encoded image bytes");
4852
DEFINE_uint64(
4953
max_input_frame_bytes,
@@ -67,7 +71,8 @@ create_muse_glimmer_batching_runtime() {
6771
FLAGS_max_inflight_requests <= 0 || FLAGS_max_decode_sequences <= 0 ||
6872
FLAGS_prefix_cache_entries < 0 || FLAGS_max_image_bytes == 0 ||
6973
FLAGS_prefix_checkpoint_interval < 0 || FLAGS_prefix_capture_slots < 0 ||
70-
(FLAGS_prefix_checkpoint_interval > 0 &&
74+
((FLAGS_prefix_checkpoint_interval > 0 ||
75+
FLAGS_prefix_capture_prompt_end) &&
7176
FLAGS_prefix_cache_entries == 0) ||
7277
FLAGS_max_image_bytes > 20 * 1024 * 1024 ||
7378
FLAGS_max_input_frame_bytes == 0 ||
@@ -84,6 +89,7 @@ create_muse_glimmer_batching_runtime() {
8489
serving.prefix_cache_capacity = FLAGS_prefix_cache_entries;
8590
serving.max_prefix_captures = FLAGS_prefix_capture_slots;
8691
serving.prefix_checkpoint_interval = FLAGS_prefix_checkpoint_interval;
92+
serving.prefix_capture_prompt_end = FLAGS_prefix_capture_prompt_end;
8793
const int64_t physical = static_cast<int64_t>(FLAGS_max_sessions) +
8894
FLAGS_prefix_cache_entries +
8995
static_cast<int64_t>(serving.resolved_prefix_captures());

‎examples/models/muse-glimmer/serving/serve_batching.py‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,16 @@ def _parse_args(argv=None):
7474
help="Transient capture rows; 0 (default): size from session/request limits",
7575
)
7676
parser.add_argument("--max-vision-patches", type=int, default=4096)
77+
parser.add_argument(
78+
"--prefix-capture-prompt-end",
79+
action="store_true",
80+
help="Retain each request's prompt end in the bounded prefix cache",
81+
)
7782
parser.add_argument(
7883
"--prefix-checkpoint-interval",
7984
type=int,
8085
default=0,
81-
help="Checkpoint interval in tokens; 0: initial prompt only. Uses global LRU.",
86+
help="Periodic checkpoint interval in tokens; 0 disables periodic capture.",
8287
)
8388
parser.add_argument("--max-image-bytes", type=int, default=_MAX_IMAGE_BYTES)
8489
parser.add_argument(
@@ -93,7 +98,12 @@ def _parse_args(argv=None):
9398
parser.add_argument("--host", default="127.0.0.1")
9499
parser.add_argument("--port", type=int, default=8000)
95100
args = parser.parse_args(argv)
96-
_validate_limits(parser, args, prefix_capture_slots=args.prefix_capture_slots)
101+
_validate_limits(
102+
parser,
103+
args,
104+
prefix_capture_slots=args.prefix_capture_slots,
105+
prefix_capture_prompt_end=args.prefix_capture_prompt_end,
106+
)
97107
if not 0 <= args.dflash_n_draft < _INT32_MAX:
98108
parser.error("--dflash-n-draft must be nonnegative and below INT32_MAX")
99109
if args.dflash_n_draft and not args.dflash:
@@ -169,6 +179,10 @@ def _worker_command(args):
169179
args.prefix_capture_slots if args.prefix_cache_entries else None,
170180
),
171181
("prefix_checkpoint_interval", args.prefix_checkpoint_interval or None),
182+
(
183+
"prefix_capture_prompt_end",
184+
"true" if args.prefix_capture_prompt_end else None,
185+
),
172186
("max_vision_patches", args.max_vision_patches),
173187
("max_image_bytes", args.max_image_bytes),
174188
("max_input_frame_bytes", args.max_request_bytes),

‎examples/models/muse-glimmer/tests/test_serve_batching.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@ def test_prefix_capture_override_reaches_worker(arguments, captures):
5151
assert f"--prefix_capture_slots={captures}" in serve_batching._worker_command(args)
5252

5353

54+
def test_prompt_end_capture_is_opt_in_and_reaches_worker(arguments):
55+
default = serve_batching._parse_args(arguments + ["--prefix-cache-entries", "4"])
56+
assert not default.prefix_capture_prompt_end
57+
assert not any(
58+
"prefix_capture_prompt_end" in flag
59+
for flag in serve_batching._worker_command(default)
60+
)
61+
enabled = serve_batching._parse_args(
62+
arguments + ["--prefix-cache-entries", "4", "--prefix-capture-prompt-end"]
63+
)
64+
assert "--prefix_capture_prompt_end=true" in serve_batching._worker_command(enabled)
65+
with pytest.raises(SystemExit):
66+
serve_batching._parse_args(arguments + ["--prefix-capture-prompt-end"])
67+
68+
5469
def test_worker_forwards_mg_options(arguments):
5570
args = serve_batching._parse_args(
5671
arguments

‎extension/llm/serving/serving_runtime.cpp‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ struct ServingRuntime::Impl {
262262
static_cast<std::size_t>(
263263
std::numeric_limits<batching::Position>::max()) &&
264264
((!config.prefix_checkpoint_interval &&
265+
!config.prefix_capture_prompt_end &&
265266
!config.prefix_checkpoint_policy) ||
266267
config.prefix_cache_capacity)),
267268
tokenizer_(tokenizer),
@@ -1159,6 +1160,7 @@ struct ServingRuntime::Impl {
11591160
std::function<void(batching::Position)> on_prefill;
11601161
if (request->text && request->text->prompt &&
11611162
(config_.prefix_checkpoint_interval != 0 ||
1163+
config_.prefix_capture_prompt_end ||
11621164
config_.prefix_checkpoint_policy)) {
11631165
#if ET_HAS_EXCEPTIONS
11641166
try {
@@ -1192,6 +1194,10 @@ struct ServingRuntime::Impl {
11921194
static_cast<batching::Position>(
11931195
config_.prefix_checkpoint_interval),
11941196
opening);
1197+
if (config_.prefix_capture_prompt_end && end > start &&
1198+
(breakpoints.empty() || breakpoints.back() != end)) {
1199+
breakpoints.push_back(end);
1200+
}
11951201
}
11961202
on_prefill = request->text->prefix_capture->observer(
11971203
[this, request] {

‎extension/llm/serving/serving_runtime.h‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,12 @@ struct ET_EXPERIMENTAL ServingRuntimeConfig {
6060
// Capture at absolute interval multiples and the initial prompt end.
6161
// Requires cache capacity; zero captures only the initial prompt.
6262
std::size_t prefix_checkpoint_interval = 0;
63-
// Override the interval with increasing checkpoints in (start,
64-
// prompt.size()]. Invalid or throwing policies skip capture. Requires cache
65-
// capacity. Runs on control; must not block or reenter the runtime.
63+
// Best-effort capture at each request's prompt end, including continuations
64+
// and replacements. Shares checkpoint admission; requires cache capacity.
65+
bool prefix_capture_prompt_end = false;
66+
// Override the interval and prompt-end flag with increasing checkpoints in
67+
// (start, prompt.size()]. Invalid or throwing policies skip capture. Requires
68+
// cache capacity. Runs on control; must not block or reenter the runtime.
6669
std::function<std::vector<batching::PrefixCheckpoint>(
6770
const batching::PrefixIdentity& prompt,
6871
batching::Position start,
@@ -86,7 +89,8 @@ struct ET_EXPERIMENTAL ServingRuntimeConfig {
8689
return max_prefix_captures;
8790
}
8891
const auto concurrency = std::min(max_sessions, max_requests);
89-
return prefix_checkpoint_interval || prefix_checkpoint_policy
92+
return prefix_checkpoint_interval || prefix_capture_prompt_end ||
93+
prefix_checkpoint_policy
9094
? std::min(concurrency, std::size_t{2})
9195
: concurrency;
9296
}

0 commit comments

Comments
 (0)