Skip to content

Commit 83986e5

Browse files
committed
[UPDATE] Update
[ghstack-poisoned]
2 parents 950096c + 2dae19c commit 83986e5

6 files changed

Lines changed: 218 additions & 179 deletions

File tree

extension/llm/runner/llm_session.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,11 @@ class ET_EXPERIMENTAL LLMSession {
9090
virtual ::executorch::runtime::Result<DecodeResult> decode_one(
9191
const SamplingConfig& sampling) = 0;
9292

93-
/// Rewind the KV cache to `pos` (prefix reuse). Valid for full-KV models;
94-
/// sliding-window KV may reject a seek past its window (the caller falls back
95-
/// to a fresh prefill).
93+
/// Rewind the KV cache to `pos` (prefix reuse). Valid for full-KV models.
94+
/// Returns InvalidArgument if `pos` is outside [0, position()]. Returns
95+
/// NotSupported for models whose state cannot be safely rewound (for example,
96+
/// non-KV-cache, sliding-window, or recurrent-state models); callers should
97+
/// fall back to reset() + full prefill.
9698
virtual ::executorch::runtime::Error seek(int64_t pos) = 0;
9799

98100
/// Number of tokens with resident KV (upper bound for seek()).

extension/llm/runner/util.h

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,14 @@ ET_EXPERIMENTAL size_t inline utf8_complete_prefix_len(const std::string& s) {
101101

102102
// How many leading bytes of `text` a streaming consumer may safely emit given a
103103
// set of `stops` strings, and whether a stop was hit (`stop_hit`).
104-
// * If any stop occurs, returns the byte offset of the EARLIEST occurrence and
105-
// sets stop_hit=true — text before it is safe; the stop and everything after
106-
// are dropped (the stop is excluded from output).
104+
// * If any stop occurs, returns the byte offset of the EARLIEST occurrence
105+
// and
106+
// sets stop_hit=true — text before it is safe; the stop and everything
107+
// after are dropped (the stop is excluded from output).
107108
// * Otherwise returns the length minus the longest possible partial-stop tail
108-
// (max(len(stop))-1 bytes), snapped DOWN to a UTF-8 boundary so a multi-byte
109-
// character is never split; stop_hit=false. Holding back that tail lets a
110-
// stop that straddles the next piece still be caught.
109+
// (max(len(stop))-1 bytes), snapped DOWN to a UTF-8 boundary so a
110+
// multi-byte character is never split; stop_hit=false. Holding back that
111+
// tail lets a stop that straddles the next piece still be caught.
111112
// `text` is expected to be complete-UTF-8 (e.g. the assembled output of
112113
// utf8_complete_prefix_len). Empty `stops` => emit everything, no hold-back.
113114
ET_EXPERIMENTAL size_t inline stop_safe_prefix_len(

extension/llm/server/cpp/text_llm_worker.cpp

Lines changed: 9 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -9,167 +9,28 @@
99
// Generic model-execution worker for standard .pte TextLLM models.
1010
//
1111
// All model execution lives here in C++ (via TextLLMEngine / TextLLMSession,
12-
// the stable serving abstraction) — there is no Python model code, no pybind,
13-
// and no in-process Python serving. The OpenAI control plane (Python) spawns
14-
// this process and drives it over JSONL on stdin/stdout (see worker_client.py).
15-
//
16-
// Protocol (one JSON object per line; identical to worker_client.py):
17-
// worker -> stdout, once: {"ready": true}
18-
// client -> stdin, per request: {"prompt": str, "max_new_tokens": int,
19-
// "temperature": float, "stop": [str, ...]}
20-
// worker -> stdout, per request: {"token": str} * (streamed)
21-
// {"done": true, "prompt_tokens": int,
22-
// "completion_tokens": int,
23-
// "finish_reason": "stop" | "length"}
24-
// or {"error": str}
25-
//
26-
// stdout carries ONLY protocol JSON; all logs go to stderr (ET_LOG). One
27-
// request at a time (the control plane serializes; one worker == one session).
28-
//
29-
// V1 cancellation: a request runs to completion (EOS or max_new_tokens); it is
30-
// NOT interruptible mid-generation. The control plane may abandon the response
31-
// on client disconnect, but the worker finishes the in-flight request. (A
32-
// cooperative stop would need a separate control channel; out of scope for V1.)
12+
// the stable serving abstraction) — no Python model code, no pybind, no
13+
// in-process Python serving. The OpenAI control plane (Python) spawns this
14+
// process and drives it over JSONL on stdin/stdout (see worker_client.py). The
15+
// JSONL protocol and the decode loop are shared across all workers in
16+
// worker_loop.h; this file only constructs the engine/session/tokenizer.
3317

3418
#include <gflags/gflags.h>
35-
#include <nlohmann/json.hpp>
3619

37-
#include <executorch/extension/llm/runner/constants.h>
3820
#include <executorch/extension/llm/runner/llm_runner_helper.h>
3921
#include <executorch/extension/llm/runner/llm_session.h>
40-
#include <executorch/extension/llm/runner/util.h>
22+
#include <executorch/extension/llm/server/cpp/worker_loop.h>
4123
#include <executorch/runtime/platform/log.h>
4224

43-
#include <algorithm>
44-
#include <cstdint>
45-
#include <iostream>
46-
#include <string>
47-
#include <vector>
25+
#include <optional>
26+
#include <utility>
4827

4928
DEFINE_string(model_path, "", "Self-contained model .pte file path.");
5029
DEFINE_string(tokenizer_path, "", "HuggingFace tokenizer.json path.");
5130

5231
namespace {
53-
5432
namespace llm = ::executorch::extension::llm;
5533
using ::executorch::runtime::Error;
56-
using json = nlohmann::json;
57-
58-
// Emit one protocol object as a JSON line on stdout. error_handler::replace
59-
// keeps a stray invalid UTF-8 byte (byte-level BPE) from aborting
60-
// serialization.
61-
void emit(const json& obj) {
62-
std::cout << obj.dump(-1, ' ', false, json::error_handler_t::replace) << "\n";
63-
std::cout.flush();
64-
}
65-
66-
// One generation request: reset the session, encode the prompt, prefill, then
67-
// loop decode_one() streaming complete-UTF-8 text pieces. Mirrors the retired
68-
// Python SessionGenerateAdapter: a terminal step (EOS or stop) ends generation
69-
// and is not emitted or counted.
70-
void handle_request(
71-
llm::LLMSession& session,
72-
::tokenizers::Tokenizer& tokenizer,
73-
const std::unordered_map<std::string, int64_t>& metadata,
74-
const json& req) {
75-
const std::string prompt = req.at("prompt").get<std::string>();
76-
int64_t max_new = req.value("max_new_tokens", static_cast<int64_t>(-1));
77-
const float temperature = req.value("temperature", 0.0f);
78-
// Stop strings (the request's `stop` sequences): terminate at the token
79-
// boundary where one appears so we don't generate to EOS/max_new past it. The
80-
// control plane also enforces these as a backstop.
81-
const std::vector<std::string> stops =
82-
req.value("stop", std::vector<std::string>{});
83-
84-
if (session.reset() != Error::Ok) {
85-
throw std::runtime_error("session reset failed");
86-
}
87-
// No special tokens: the prompt is already rendered (the control plane
88-
// applied the chat template), matching the runner's own encode path.
89-
auto encode_result = tokenizer.encode(prompt, /*bos=*/0, /*eos=*/0);
90-
if (!encode_result.ok()) {
91-
throw std::runtime_error("prompt encode failed");
92-
}
93-
std::vector<uint64_t> ids = std::move(*encode_result);
94-
if (ids.empty()) {
95-
throw std::runtime_error("empty prompt");
96-
}
97-
const int64_t num_prompt = static_cast<int64_t>(ids.size());
98-
99-
// Bound generation to the context window: default to filling the remaining
100-
// room, and clamp an explicit max_new_tokens too, so decode never steps past
101-
// the window (which would error mid-generation after partial output).
102-
const auto ctx_it = metadata.find(llm::kMaxContextLen);
103-
if (ctx_it != metadata.end()) {
104-
const int64_t room = ctx_it->second - num_prompt;
105-
if (room <= 0) {
106-
throw std::runtime_error(
107-
"prompt fills the context window; no room to generate");
108-
}
109-
if (max_new <= 0 || max_new > room) {
110-
max_new = room;
111-
}
112-
} else if (max_new <= 0) {
113-
max_new = 2048;
114-
}
115-
116-
llm::SamplingConfig sampling;
117-
sampling.temperature = temperature;
118-
if (session.prefill_tokens(std::move(ids), &sampling) != Error::Ok) {
119-
throw std::runtime_error("prefill failed");
120-
}
121-
122-
std::string buf; // bytes not yet forming a complete UTF-8 prefix
123-
std::string pending; // complete-UTF-8 text held back for stop-string matching
124-
int64_t num_generated = 0;
125-
std::string finish = "length"; // EOS or stop string -> "stop"
126-
bool stop_string = false; // a request stop string was matched
127-
for (int64_t step = 0; step < max_new; ++step) {
128-
auto step_result = session.decode_one(sampling);
129-
if (step_result.error() != Error::Ok) {
130-
throw std::runtime_error("decode failed");
131-
}
132-
const auto& d = step_result.get();
133-
if (d.is_terminal) {
134-
finish = "stop";
135-
break; // terminal step (EOS / cooperative stop): not emitted or counted
136-
}
137-
++num_generated;
138-
buf += d.text_piece;
139-
const size_t cut = llm::utf8_complete_prefix_len(buf);
140-
if (cut > 0) {
141-
pending += buf.substr(0, cut);
142-
buf.erase(0, cut);
143-
}
144-
bool stop_hit = false;
145-
const size_t safe = llm::stop_safe_prefix_len(pending, stops, stop_hit);
146-
if (safe > 0) {
147-
emit({{"token", pending.substr(0, safe)}});
148-
pending.erase(0, safe);
149-
}
150-
if (stop_hit) {
151-
finish = "stop"; // reached a stop string: drop it and everything after
152-
stop_string = true;
153-
break;
154-
}
155-
}
156-
if (!stop_string) {
157-
// EOS or length: flush held-back text + any trailing incomplete bytes
158-
// (replaced if invalid). A stop-string hit drops the remainder instead.
159-
pending += buf;
160-
if (!pending.empty()) {
161-
emit({{"token", pending}});
162-
}
163-
}
164-
// finish_reason: "stop" if the model emitted EOS or hit a stop string, else
165-
// "length" — it ran to max_new (possibly clamped to the context window).
166-
emit(
167-
{{"done", true},
168-
{"prompt_tokens", num_prompt},
169-
{"completion_tokens", num_generated},
170-
{"finish_reason", finish}});
171-
}
172-
17334
} // namespace
17435

17536
int main(int argc, char** argv) {
@@ -203,22 +64,6 @@ int main(int argc, char** argv) {
20364
ET_LOG(Error, "text_llm_worker: failed to load tokenizer");
20465
return 1;
20566
}
206-
const auto& metadata = engine->metadata();
20767

208-
emit({{"ready", true}});
209-
210-
std::string line;
211-
while (std::getline(std::cin, line)) {
212-
if (line.empty()) {
213-
continue;
214-
}
215-
try {
216-
handle_request(
217-
*session, *tokenizer, metadata, nlohmann::json::parse(line));
218-
} catch (
219-
const std::exception& e) { // report to the control plane, keep serving
220-
emit({{"error", std::string(e.what())}});
221-
}
222-
}
223-
return 0;
68+
return llm::run_worker_stdio_loop(*session, *tokenizer, engine->metadata());
22469
}

0 commit comments

Comments
 (0)