Skip to content

Commit ffb3ce7

Browse files
committed
[LLM] Extract text-serving contracts and helpers
Prepare a batching-backed serving path without changing current worker behavior. Share the existing prompt-history planner and define experimental serving types for subsequent runtime and transport changes. Test Plan: All 8 existing worker prompt-planner cases passed. The serving contract and planner headers passed standalone C++17 compilation. Behavioral serving tests are introduced alongside their implementations in later commits. Authored with AI assistance from Devmate.
1 parent ffb13bf commit ffb3ce7

8 files changed

Lines changed: 220 additions & 62 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/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: 2 additions & 0 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
],

‎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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
install(TARGETS extension_llm_serving EXPORT ExecuTorchTargets)
14+
install(FILES types.h prompt_history.h
15+
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/executorch/extension/llm/serving
16+
)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 <vector>
20+
21+
#include <executorch/runtime/platform/compiler.h>
22+
23+
namespace executorch {
24+
namespace extension {
25+
namespace llm {
26+
27+
struct ET_EXPERIMENTAL PrefillPlan {
28+
enum Action {
29+
kFull, // reset + prefill the whole prompt
30+
kSuffix // keep state, prefill prompt_ids[suffix_start:] at pos>0
31+
} action;
32+
size_t suffix_start; // index in prompt_ids where prefill begins (0 for kFull)
33+
// Reported as session_reset_reason: "new" (no resident), "exact_prefix"
34+
// (suffix reuse), "dirty", "mismatch", "equal" (prompt == resident).
35+
const char* reason;
36+
};
37+
38+
ET_EXPERIMENTAL inline PrefillPlan plan_prefill(
39+
const std::vector<uint64_t>& resident,
40+
const std::vector<uint64_t>& prompt,
41+
bool dirty) {
42+
if (dirty) {
43+
return {PrefillPlan::kFull, 0, "dirty"};
44+
}
45+
if (resident.empty()) {
46+
return {PrefillPlan::kFull, 0, "new"};
47+
}
48+
if (prompt.size() < resident.size()) {
49+
return {PrefillPlan::kFull, 0, "mismatch"};
50+
}
51+
for (size_t i = 0; i < resident.size(); ++i) {
52+
if (prompt[i] != resident[i]) {
53+
return {PrefillPlan::kFull, 0, "mismatch"};
54+
}
55+
}
56+
if (prompt.size() == resident.size()) {
57+
// Equal histories conservatively replay: this helper cannot establish that
58+
// decoding without a non-empty suffix is supported or safe for the caller.
59+
return {PrefillPlan::kFull, 0, "equal"};
60+
}
61+
return {PrefillPlan::kSuffix, resident.size(), "exact_prefix"};
62+
}
63+
64+
} // namespace llm
65+
} // namespace extension
66+
} // namespace executorch

‎extension/llm/serving/types.h‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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+
#include <cstddef>
12+
#include <cstdint>
13+
#include <optional>
14+
#include <string>
15+
#include <variant>
16+
#include <vector>
17+
18+
#include <executorch/extension/llm/batching/types.h>
19+
#include <executorch/runtime/platform/compiler.h>
20+
21+
namespace executorch {
22+
namespace extension {
23+
namespace llm {
24+
namespace serving {
25+
26+
struct ET_EXPERIMENTAL TextSegment {
27+
std::string text;
28+
};
29+
30+
struct ET_EXPERIMENTAL TokenIdsSegment {
31+
std::vector<batching::Token> ids;
32+
};
33+
34+
// Extend this variant with typed segments when another modality is supported.
35+
using PromptSegment ET_EXPERIMENTAL =
36+
std::variant<TextSegment, TokenIdsSegment>;
37+
38+
// The full prompt, not a session delta. Preparation encodes each text segment
39+
// separately and appends ID segments verbatim, in order, without adding
40+
// BOS/EOS. Segment boundaries therefore matter even between adjacent text
41+
// segments.
42+
struct ET_EXPERIMENTAL PromptInput {
43+
std::vector<PromptSegment> segments;
44+
};
45+
46+
struct ET_EXPERIMENTAL GenerationOptions {
47+
// Unset uses the available context, or the service default if unknown.
48+
std::optional<std::int32_t> max_new_tokens;
49+
batching::SamplingParams sampling;
50+
std::vector<batching::Token> stop_tokens;
51+
std::vector<std::string> stop_strings;
52+
std::optional<std::uint64_t> seed;
53+
};
54+
55+
enum class ET_EXPERIMENTAL FinishReason {
56+
Stop, // EOS, a stop token, or a stop string
57+
Length,
58+
Cancelled,
59+
Failed,
60+
};
61+
62+
enum class ET_EXPERIMENTAL ErrorCode {
63+
InvalidArgument,
64+
NotReady,
65+
SessionNotFound,
66+
SessionBusy,
67+
CapacityExceeded,
68+
Internal,
69+
};
70+
71+
struct ET_EXPERIMENTAL ServingError {
72+
ErrorCode code = ErrorCode::Internal;
73+
std::string message;
74+
};
75+
76+
struct ET_EXPERIMENTAL GenerationStats {
77+
std::size_t prompt_tokens = 0;
78+
// Non-terminal generated tokens, including tokens hidden by a string stop.
79+
// Neither this count nor visible text describes the session's logical
80+
// history.
81+
std::size_t completion_tokens = 0;
82+
std::size_t reused_prompt_tokens = 0;
83+
// Actually consumed prompt tokens; may be partial on cancellation or failure.
84+
std::size_t prefilled_prompt_tokens = 0;
85+
double prefill_ms = 0.0;
86+
double decode_ms = 0.0;
87+
double total_ms = 0.0;
88+
std::string session_reset_reason;
89+
// Absent when output trimming or cancellation makes exact replay unsafe.
90+
std::optional<std::vector<batching::Token>> generated_token_ids;
91+
};
92+
93+
struct ET_EXPERIMENTAL TextEvent {
94+
std::string text;
95+
};
96+
97+
struct ET_EXPERIMENTAL TerminalEvent {
98+
FinishReason finish_reason = FinishReason::Stop;
99+
GenerationStats stats;
100+
// Present exactly when finish_reason is Failed.
101+
std::optional<ServingError> error;
102+
};
103+
104+
// Per-request order: zero or more text events, then exactly one terminal event.
105+
using GenerationEvent ET_EXPERIMENTAL = std::variant<TextEvent, TerminalEvent>;
106+
107+
struct ET_EXPERIMENTAL ServingInfo {
108+
bool ready = false;
109+
std::size_t max_context_length = 0;
110+
std::size_t max_sessions = 0;
111+
// Logical serving slots, including slots reserved during reset/reopen.
112+
// This is not a count of physically live executor sessions.
113+
std::size_t active_sessions = 0;
114+
};
115+
116+
} // namespace serving
117+
} // namespace llm
118+
} // namespace extension
119+
} // namespace executorch

0 commit comments

Comments
 (0)