Skip to content

Commit a0ca769

Browse files
authored
[LLM serving 1/x] Define text-serving contracts and share prompt planning (pytorch#23270)
Prepare a batching-backed serving layer without changing existing worker behavior. Introduce experimental C++ types for text and exact-token prompts, generation options, streamed text and terminal events, errors, usage statistics, and serving metadata. Reuse batching’s token and sampling types. Move the existing exact-token prompt-history planner into the shared serving layer, retaining the legacy worker’s include path and unchanged continuation/replay decisions. Add CMake and Buck build wiring. This PR introduces contracts and shared planning only. Runtime implementation and behavioral tests follow in subsequent PRs; no scheduling or generation behavior changes here.
1 parent c5f2b7a commit a0ca769

9 files changed

Lines changed: 270 additions & 67 deletions

File tree

‎CMakeLists.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,6 +1015,8 @@ if(EXECUTORCH_BUILD_EXTENSION_LLM)
10151015
list(APPEND _executorch_extensions extension_llm_batching
10161016
extension_llm_batching_module
10171017
)
1018+
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/extension/llm/serving)
1019+
list(APPEND _executorch_extensions extension_llm_serving)
10181020
endif()
10191021

10201022
if(EXECUTORCH_BUILD_EXTENSION_RUNNER_UTIL)

‎examples/llm_server/cpp/BUCK‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ fbcode_target(_kind = runtime.cxx_library,
1313
header_namespace = "executorch/examples/llm_server/cpp",
1414
exported_deps = [
1515
"//executorch/extension/llm/runner:runner_lib",
16+
"//executorch/extension/llm/serving:prompt_history",
1617
"//pytorch/tokenizers:headers",
1718
],
1819
external_deps = ["nlohmann_json"],

‎examples/llm_server/cpp/test_worker_prefill_plan.cpp‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
#include <executorch/examples/llm_server/cpp/worker_prefill_plan.h>
1414

1515
#include <cstdio>
16-
#include <cstring>
1716
#include <string>
1817
#include <vector>
1918

@@ -30,15 +29,15 @@ void expect(
3029
size_t suffix_start,
3130
const char* reason) {
3231
bool ok = p.action == action && p.suffix_start == suffix_start &&
33-
std::strcmp(p.reason, reason) == 0;
32+
p.reason == reason;
3433
if (!ok) {
3534
++g_failures;
3635
printf(
3736
" [FAIL] %s: got action=%d suffix_start=%zu reason=%s\n",
3837
name,
3938
(int)p.action,
4039
p.suffix_start,
41-
p.reason);
40+
p.reason.c_str());
4241
} else {
4342
printf(" [PASS] %s\n", name);
4443
}

‎examples/llm_server/cpp/worker_prefill_plan.h‎

Lines changed: 1 addition & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -8,65 +8,4 @@
88

99
#pragma once
1010

11-
// Pure warm-resume prefill decision for the model worker (no model/session/ET
12-
// dependency, so it is unit-testable in isolation). Given a named session's
13-
// resident token ids (exactly the tokens currently in its KV/recurrent state),
14-
// the new request's prompt token ids, and whether the session is dirty, decide
15-
// whether to reset + full-prefill or to keep the state and prefill only the
16-
// suffix. The decision is exact-token (never string / retokenized text), so a
17-
// kSuffix plan is always a correct continuation; anything uncertain falls back
18-
// to kFull. See worker_loop.h for how the plan is executed.
19-
20-
#include <cstddef>
21-
#include <cstdint>
22-
#include <vector>
23-
24-
namespace executorch {
25-
namespace extension {
26-
namespace llm {
27-
28-
struct PrefillPlan {
29-
enum Action {
30-
kFull, // reset + prefill the whole prompt
31-
kSuffix // keep state, prefill prompt_ids[suffix_start:] at pos>0
32-
} action;
33-
size_t suffix_start; // index in prompt_ids where prefill begins (0 for kFull)
34-
// Reported as session_reset_reason: "new" (no resident), "exact_prefix"
35-
// (suffix reuse), "dirty", "mismatch", "equal" (prompt == resident).
36-
const char* reason;
37-
};
38-
39-
inline PrefillPlan plan_prefill(
40-
const std::vector<uint64_t>& resident,
41-
const std::vector<uint64_t>& prompt,
42-
bool dirty) {
43-
if (dirty) {
44-
return {PrefillPlan::kFull, 0, "dirty"};
45-
}
46-
if (resident.empty()) {
47-
return {PrefillPlan::kFull, 0, "new"};
48-
}
49-
if (prompt.size() < resident.size()) {
50-
return {PrefillPlan::kFull, 0, "mismatch"};
51-
}
52-
for (size_t i = 0; i < resident.size(); ++i) {
53-
if (prompt[i] != resident[i]) {
54-
return {PrefillPlan::kFull, 0, "mismatch"};
55-
}
56-
}
57-
if (prompt.size() == resident.size()) {
58-
// Prompt is exactly the resident state (no new tokens). The ideal would be
59-
// to skip prefill and decode straight from the session's pending token, but
60-
// the LLMSession API exposes no "is there a valid pending token?" query, so
61-
// we conservatively reset + full prefill rather than risk
62-
// prefill_tokens([]) or decoding from a stale/absent pending. Rare in
63-
// practice (a new turn adds tokens); a session pending-state query is a
64-
// possible later optimization.
65-
return {PrefillPlan::kFull, 0, "equal"};
66-
}
67-
return {PrefillPlan::kSuffix, resident.size(), "exact_prefix"};
68-
}
69-
70-
} // namespace llm
71-
} // namespace extension
72-
} // namespace executorch
11+
#include <executorch/extension/llm/serving/prompt_history.h>

‎extension/llm/runner/targets.bzl‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ def define_common_targets():
3737
exported_headers = [
3838
"model_metadata.h",
3939
"stats.h",
40+
"text_stream.h",
4041
"util.h",
4142
],
4243
visibility = ["PUBLIC"],
4344
exported_deps = [
4445
":constants",
46+
"//pytorch/tokenizers:headers",
4547
"//executorch/extension/module:module" + aten_suffix,
4648
"//executorch/extension/tensor:tensor" + aten_suffix,
4749
],
@@ -101,12 +103,23 @@ def define_common_targets():
101103
)
102104

103105
runtime.cxx_library(
104-
name = "multimodal_runner_lib" + aten_suffix,
106+
name = "multimodal_input" + aten_suffix,
105107
exported_headers = [
106108
"audio.h",
107109
"image.h",
108-
"wav_loader.h",
109110
"multimodal_input.h",
111+
],
112+
visibility = ["PUBLIC"],
113+
exported_deps = [
114+
"//executorch/extension/tensor:tensor" + aten_suffix,
115+
"//executorch/runtime/platform:platform",
116+
],
117+
)
118+
119+
runtime.cxx_library(
120+
name = "multimodal_runner_lib" + aten_suffix,
121+
exported_headers = [
122+
"wav_loader.h",
110123
"multimodal_runner.h",
111124
"multimodal_prefiller.h",
112125
"multimodal_decoder_runner.h",
@@ -118,6 +131,7 @@ def define_common_targets():
118131
":text_decoder_runner" + aten_suffix,
119132
":text_prefiller" + aten_suffix,
120133
":image_prefiller" + aten_suffix,
134+
":multimodal_input" + aten_suffix,
121135
":text_token_generator" + aten_suffix,
122136
],
123137
)

‎extension/llm/serving/BUCK‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
load("@fbcode_macros//build_defs:build_file_migration.bzl", "fbcode_target")
2+
load("@fbsource//xplat/executorch/build:runtime_wrapper.bzl", "runtime")
3+
4+
oncall("executorch")
5+
6+
# The legacy worker only needs the pure planner, not serving or batching types.
7+
fbcode_target(_kind = runtime.cxx_library,
8+
name = "prompt_history",
9+
exported_headers = ["prompt_history.h"],
10+
exported_deps = ["//executorch/runtime/platform:platform"],
11+
header_namespace = "executorch/extension/llm/serving",
12+
visibility = ["PUBLIC"],
13+
)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
add_library(extension_llm_serving INTERFACE)
8+
target_compile_features(extension_llm_serving INTERFACE cxx_std_17)
9+
target_include_directories(
10+
extension_llm_serving INTERFACE ${_common_include_directories}
11+
)
12+
13+
target_link_libraries(extension_llm_serving INTERFACE executorch_core)
14+
15+
install(TARGETS extension_llm_serving EXPORT ExecuTorchTargets)
16+
install(FILES types.h prompt_history.h
17+
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/executorch/extension/llm/serving
18+
)
19+
20+
# Public serving inputs do not require the legacy runner.
21+
if(NOT EXECUTORCH_BUILD_EXTENSION_LLM_RUNNER)
22+
install(
23+
FILES ${CMAKE_CURRENT_SOURCE_DIR}/../runner/multimodal_input.h
24+
${CMAKE_CURRENT_SOURCE_DIR}/../runner/image.h
25+
${CMAKE_CURRENT_SOURCE_DIR}/../runner/audio.h
26+
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/executorch/extension/llm/runner
27+
)
28+
endif()
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#pragma once
10+
11+
// Pure exact-token continuation decision. `resident` is the history against
12+
// which the full prompt is compared. It can be committed KV/recurrent tokens,
13+
// or logical history including a pending token if the caller's continuation
14+
// path feeds that token before the suffix. Dirty or uncertain histories replay
15+
// the full prompt; this helper neither inspects nor changes execution state.
16+
17+
#include <cstddef>
18+
#include <cstdint>
19+
#include <string>
20+
#include <vector>
21+
22+
#include <executorch/runtime/platform/compiler.h>
23+
24+
namespace executorch {
25+
namespace extension {
26+
namespace llm {
27+
28+
struct ET_EXPERIMENTAL PrefillPlan {
29+
enum Action {
30+
kFull, // reset + prefill the whole prompt
31+
kSuffix // keep state, prefill prompt_ids[suffix_start:] at pos>0
32+
} action;
33+
size_t suffix_start; // index in prompt_ids where prefill begins (0 for kFull)
34+
// Reported as session_reset_reason: "new" (no resident), "exact_prefix"
35+
// (suffix reuse), "dirty", "mismatch", "equal" (prompt == resident).
36+
std::string reason;
37+
};
38+
39+
ET_EXPERIMENTAL inline PrefillPlan plan_prefill(
40+
const std::vector<uint64_t>& resident,
41+
const std::vector<uint64_t>& prompt,
42+
bool dirty) {
43+
if (dirty) {
44+
return {PrefillPlan::kFull, 0, "dirty"};
45+
}
46+
if (resident.empty()) {
47+
return {PrefillPlan::kFull, 0, "new"};
48+
}
49+
if (prompt.size() < resident.size()) {
50+
return {PrefillPlan::kFull, 0, "mismatch"};
51+
}
52+
for (size_t i = 0; i < resident.size(); ++i) {
53+
if (prompt[i] != resident[i]) {
54+
return {PrefillPlan::kFull, 0, "mismatch"};
55+
}
56+
}
57+
if (prompt.size() == resident.size()) {
58+
// Equal histories conservatively replay: this helper cannot establish that
59+
// decoding without a non-empty suffix is supported or safe for the caller.
60+
return {PrefillPlan::kFull, 0, "equal"};
61+
}
62+
return {PrefillPlan::kSuffix, resident.size(), "exact_prefix"};
63+
}
64+
65+
} // namespace llm
66+
} // namespace extension
67+
} // namespace executorch

0 commit comments

Comments
 (0)