Skip to content

Commit 78e0384

Browse files
authored
llm_runner: add Engine and Session interfaces (#20243)
The LLM serving path needs a stable contract between generic serving code and model-specific execution code. TextLLM, Qwen, Gemma, CUDA, and future backends all differ in how they own weights and mutable state, but the server should not know those details or grow a new Python binding for every model. This introduces the minimal runner-level split needed for that contract. LLMEngine represents the loaded physical model and its serving capacity; LLMSession represents one logical conversation state and exposes reset/prefill/decode-style operations. That shape lets a worker drive different model implementations through one interface while keeping KV/recurrent/cache ownership inside C++. This commit is only the interface and build export. It deliberately does not add a concrete adapter or change existing runner behavior, so model migrations and serving can be reviewed as downstream uses of the contract rather than hidden side effects. #20001
1 parent 182be0e commit 78e0384

3 files changed

Lines changed: 155 additions & 0 deletions

File tree

extension/llm/runner/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ include(${EXECUTORCH_ROOT}/tools/cmake/Codegen.cmake)
2828
#
2929
executorch_load_build_variables()
3030

31+
add_library(extension_llm_session INTERFACE)
32+
target_link_libraries(extension_llm_session INTERFACE executorch_core)
33+
target_include_directories(
34+
extension_llm_session INTERFACE ${_common_include_directories}
35+
)
36+
3137
# build llm runner library
3238
list(TRANSFORM _extension_llm_runner__srcs PREPEND "${EXECUTORCH_ROOT}/")
3339

@@ -81,6 +87,7 @@ if(EXECUTORCH_BUILD_CUDA)
8187
endif()
8288
endif()
8389

90+
install(TARGETS extension_llm_session EXPORT ExecuTorchTargets)
8491
install(
8592
TARGETS extension_llm_runner
8693
EXPORT ExecuTorchTargets

extension/llm/runner/llm_session.h

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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+
// Engine/Session interfaces for model-specific LLM implementations. LLMEngine
10+
// owns loaded model resources; LLMSession owns one logical generation state.
11+
// Higher-level generation APIs can wrap this lower-level token-step contract.
12+
13+
#pragma once
14+
15+
#include <cstdint>
16+
#include <memory>
17+
#include <string>
18+
#include <unordered_map>
19+
#include <vector>
20+
21+
#include <executorch/runtime/core/result.h>
22+
#include <executorch/runtime/platform/compiler.h>
23+
24+
namespace executorch::extension::llm {
25+
26+
/// Per-decode sampling parameters. Implementations apply supported fields and
27+
/// reject unsupported non-default values rather than silently ignoring them. -1
28+
/// temperature means implementation default.
29+
struct ET_EXPERIMENTAL SamplingConfig {
30+
float temperature = -1.0f;
31+
float top_p = 1.0f;
32+
int32_t top_k = 0; // 0 = disabled
33+
uint64_t seed = 0; // 0 = unset
34+
};
35+
36+
/// One decoded step: the exact sampled token id and its decoded text piece
37+
/// (raw bytes; may be a partial UTF-8 sequence the caller assembles).
38+
///
39+
/// `is_eos` is literal: the sampled token is an end-of-sequence token (use it
40+
/// for the "stop" finish reason, metrics, or accounting). `is_terminal` is
41+
/// the loop signal: generation ended at this step, either EOS or a cooperative
42+
/// stop() took effect. A decode loop should end when is_terminal is set; every
43+
/// EOS step is also terminal, but a stop step is terminal without being EOS.
44+
///
45+
/// For a cooperative stop step (requested via stop()), no token is forwarded,
46+
/// position() must not advance, `token_id` must be 0, and `text_piece` must be
47+
/// empty.
48+
struct ET_EXPERIMENTAL DecodeResult {
49+
uint64_t token_id = 0;
50+
std::string text_piece;
51+
bool is_eos = false;
52+
bool is_terminal = false;
53+
};
54+
55+
/// How many physical sessions an engine can host without silently multiplying
56+
/// model memory. This is an engine-level capacity contract, distinct from how a
57+
/// session advances a conversation.
58+
struct ET_EXPERIMENTAL LLMServingCapacity {
59+
// Physical sessions creatable without duplicating model weights.
60+
int32_t max_physical_sessions_without_weight_duplication = 1;
61+
// Estimated device memory added per session, or 0 if unknown.
62+
int64_t estimated_bytes_per_session = 0;
63+
};
64+
65+
/// One logical generation state's mutable buffers and position cursor. Created
66+
/// by an LLMEngine.
67+
class ET_EXPERIMENTAL LLMSession {
68+
public:
69+
virtual ~LLMSession() = default;
70+
71+
/// Prefill pre-tokenized input at the current position. Must be non-empty and
72+
/// fit the context window.
73+
///
74+
/// `initial_sampling` is for implementations that sample the first generated
75+
/// token during prefill. Implementations that sample only in decode_one() may
76+
/// ignore null/default configs, but should reject unsupported non-default
77+
/// fields.
78+
///
79+
/// ERROR CONTRACT: an error may be returned AFTER backend state has already
80+
/// mutated. On any error from prefill_tokens()/decode_one(), the session is
81+
/// POISONED -- position() may no longer agree with resident state. The
82+
/// caller must call reset() (and only proceed once it returns Ok) before any
83+
/// further prefill/decode; it must NOT retry the failed call.
84+
ET_NODISCARD virtual ::executorch::runtime::Error prefill_tokens(
85+
const std::vector<uint64_t>& tokens,
86+
const SamplingConfig* initial_sampling = nullptr) = 0;
87+
88+
/// Decode one token from the pending state; looping reproduces a full
89+
/// generation while returning exact sampled token ids. A normal decode_one()
90+
/// runs one forward pass and is not interruptible mid-call. If stop() is
91+
/// pending, decode_one() instead returns the synthetic terminal stop result
92+
/// documented on DecodeResult without forwarding a token.
93+
/// On error the session is poisoned -- see the error contract on
94+
/// prefill_tokens() (reset() before any further use; never retry).
95+
ET_NODISCARD virtual ::executorch::runtime::Result<DecodeResult> decode_one(
96+
const SamplingConfig& sampling) = 0;
97+
98+
/// Current logical token position for this session.
99+
virtual int64_t position() const = 0;
100+
101+
/// Clear mutable state and position for a fresh conversation.
102+
ET_NODISCARD virtual ::executorch::runtime::Error reset() = 0;
103+
104+
/// Request that a decode_one() loop stop. This is a TOKEN-BOUNDARY,
105+
/// cooperative stop: it is safe to call from another thread, but it does not
106+
/// abort a decode_one() that is already running. It takes effect at the next
107+
/// decode_one(), which then returns a terminal step (is_terminal set, is_eos
108+
/// false) without forwarding a new token. For that synthetic step, token_id
109+
/// is 0, text_piece is empty, and position() does not advance. The stop is
110+
/// cleared by the next prefill_tokens() or reset().
111+
virtual void stop() = 0;
112+
};
113+
114+
/// Holds immutable model resources once and creates isolated sessions. How
115+
/// many sessions can be created without duplicating model weights is
116+
/// backend-dependent; see serving_capacity().
117+
class ET_EXPERIMENTAL LLMEngine {
118+
public:
119+
virtual ~LLMEngine() = default;
120+
121+
/// Build a new session that reuses this engine's model resources and owns
122+
/// its own mutable generation state.
123+
ET_NODISCARD virtual ::executorch::runtime::Result<
124+
std::unique_ptr<LLMSession>>
125+
create_session() = 0;
126+
127+
/// How many physical sessions this engine can host without duplicating
128+
/// weights, plus an optional per-session memory estimate.
129+
virtual LLMServingCapacity serving_capacity() const = 0;
130+
131+
/// Model metadata such as context length and tokenizer-specific IDs.
132+
virtual const std::unordered_map<std::string, int64_t>& metadata() const = 0;
133+
};
134+
135+
} // namespace executorch::extension::llm

extension/llm/runner/targets.bzl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ def define_common_targets():
1717
visibility = ["PUBLIC"],
1818
)
1919

20+
runtime.cxx_library(
21+
name = "llm_session",
22+
exported_headers = [
23+
"llm_session.h",
24+
],
25+
visibility = ["PUBLIC"],
26+
exported_deps = [
27+
"//executorch/runtime/core:core",
28+
"//executorch/runtime/platform:platform",
29+
],
30+
)
31+
2032
for aten in get_aten_mode_options():
2133
aten_suffix = "_aten" if aten else ""
2234

@@ -128,6 +140,7 @@ def define_common_targets():
128140
exported_deps = [
129141
":image_prefiller" + aten_suffix,
130142
":irunner",
143+
":llm_session",
131144
":multimodal_runner_lib" + aten_suffix,
132145
":text_decoder_runner" + aten_suffix,
133146
":text_prefiller" + aten_suffix,

0 commit comments

Comments
 (0)