|
9 | 9 | // Generic model-execution worker for standard .pte TextLLM models. |
10 | 10 | // |
11 | 11 | // 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. |
33 | 17 |
|
34 | 18 | #include <gflags/gflags.h> |
35 | | -#include <nlohmann/json.hpp> |
36 | 19 |
|
37 | | -#include <executorch/extension/llm/runner/constants.h> |
38 | 20 | #include <executorch/extension/llm/runner/llm_runner_helper.h> |
39 | 21 | #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> |
41 | 23 | #include <executorch/runtime/platform/log.h> |
42 | 24 |
|
43 | | -#include <algorithm> |
44 | | -#include <cstdint> |
45 | | -#include <iostream> |
46 | | -#include <string> |
47 | | -#include <vector> |
| 25 | +#include <optional> |
| 26 | +#include <utility> |
48 | 27 |
|
49 | 28 | DEFINE_string(model_path, "", "Self-contained model .pte file path."); |
50 | 29 | DEFINE_string(tokenizer_path, "", "HuggingFace tokenizer.json path."); |
51 | 30 |
|
52 | 31 | namespace { |
53 | | - |
54 | 32 | namespace llm = ::executorch::extension::llm; |
55 | 33 | 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 | | - |
173 | 34 | } // namespace |
174 | 35 |
|
175 | 36 | int main(int argc, char** argv) { |
@@ -203,22 +64,6 @@ int main(int argc, char** argv) { |
203 | 64 | ET_LOG(Error, "text_llm_worker: failed to load tokenizer"); |
204 | 65 | return 1; |
205 | 66 | } |
206 | | - const auto& metadata = engine->metadata(); |
207 | 67 |
|
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()); |
224 | 69 | } |
0 commit comments