Skip to content

Commit a3f6ed5

Browse files
committed
[UPDATE] Update
[ghstack-poisoned]
2 parents 83986e5 + b433c1b commit a3f6ed5

11 files changed

Lines changed: 661 additions & 395 deletions

File tree

extension/llm/server/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ Hugging Face chat templates (`--hf-tokenizer`), `temperature` / `max_tokens` /
2525
`max_completion_tokens` / `stop`, Hermes tool calling by default
2626
(`<tool_call>...</tool_call>` JSON, complete calls only; model-specific launchers
2727
may select the Qwen XML format) with `tool_choice="none"`,
28-
structured API errors, and best-effort cancellation. V1 serving is single-slot
29-
(one worker, one session) with no prefix cache; KV prefix reuse, if it returns,
30-
lives inside the worker/session, not the control plane. Unsupported params (including `top_p`,
28+
structured API errors, and best-effort cancellation. One worker process with
29+
serialized execution; it hosts many isolated sessions on one weight load (warm
30+
append-only resume across turns). KV/prefix state lives inside the
31+
worker/session, not the control plane. Unsupported params (including `top_p`,
3132
`seed`, `n>1`, `reasoning_effort`, penalties, `logit_bias`, `response_format`,
3233
`logprobs`, and `tool_choice="required"`) are rejected with a structured 400
3334
rather than silently ignored. See `python/README.md` to run it and

extension/llm/server/python/README.md

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ Key flags:
6969
| `--allow-chatml-fallback` | opt into approximate ChatML when no HF tokenizer |
7070
| `--no-think` | default `enable_thinking=False` (e.g. Qwen3) |
7171
| `--max-context N` | reject over-long prompts with 400 instead of failing mid-gen |
72-
| `--num-runners N` | V1 supports **1 only** (single-slot: one worker serves one session; concurrent requests queue) |
72+
| `--num-runners N` | Worker processes — **1 only** (one worker hosts many isolated sessions on one weight load; more would duplicate weights) |
7373
| `--worker-bin PATH` | path to the `text_llm_worker` binary (default: `cmake-out/extension/llm/server/cpp/text_llm_worker`) |
7474

7575
## Use from an agent harness
@@ -101,16 +101,19 @@ pytest tests/
101101
OPENAI_BASE_URL=http://127.0.0.1:8000/v1 pytest ../conformance/test_openai_contract.py
102102
```
103103

104-
`tests/` builds a `RunnerPool` over a single `FakeRunner` worker handle, so the
104+
`tests/` builds a `SessionRuntime` over a single `FakeRunner` worker, so the
105105
real server/protocol/streaming code is tested over HTTP without a `.pte`. The
106106
worker JSONL protocol is covered separately by `tests/test_worker_client.py`.
107107

108108
## Architecture
109109

110-
Control plane (this dir, Python): server, OpenAI protocol, chat templating,
111-
streaming bridge, tool parsing — no CUDA, no model, no pybind. Data plane (C++):
112-
a worker process (`text_llm_worker`) owns one model session and does all token
113-
stepping and KV mutation; it speaks one JSON object per line on stdin/stdout.
110+
Control plane (this dir, Python): an OpenAI adapter (`serving_chat`) over a
111+
stateful `SessionRuntime` over one `WorkerClient` — server, protocol, chat
112+
templating, streaming bridge, tool parsing — no CUDA, no model, no pybind. Data
113+
plane (C++): a worker process (`text_llm_worker`) that owns all model state
114+
(many isolated sessions on one weight load, warm-resume prefix logic) and does
115+
all token stepping and KV mutation; it speaks one JSON object per line on
116+
stdin/stdout.
114117

115118
JSONL protocol (stdout carries protocol JSON only; logs go to stderr):
116119

@@ -132,9 +135,9 @@ does blocking pipe I/O on its executor thread.
132135
| `server.py` | FastAPI app, routes, CLI entrypoint, worker spawn |
133136
| `protocol.py` | OpenAI request/response schemas |
134137
| `chat_template.py` | messages (+tools) → prompt string |
135-
| `worker_client.py` | spawn a worker process + drive it over JSONL |
136-
| `runner_pool.py` | worker pool (one in-flight request per worker) + async streaming bridge |
137-
| `serving_chat.py` | `/v1/chat/completions` (streaming + non-streaming, stop, tools) |
138+
| `worker_client.py` | spawn a worker process + drive it over JSONL (raw transport) |
139+
| `session_runtime.py` | stateful runtime over one worker: open/generate/reset/close + streaming bridge |
140+
| `serving_chat.py` | `/v1/chat/completions` OpenAI adapter (streaming + non-streaming, stop, tools) |
138141
| `tool_parsers/` | Hermes/Qwen `<tool_call>` parser only |
139142
| `cpp/text_llm_worker.cpp` | the generic C++ worker binary |
140143

@@ -151,11 +154,11 @@ imports an example. Backend specifics (CUDA/AOTI, Metal) stay inside the worker.
151154
## Scope & caveats
152155

153156
Deliberately narrow (reliability-first): Hermes/Qwen tool calling only;
154-
unsupported sampling params are rejected, not ignored. V1 is **single-slot**: one
155-
worker hosts one session, so `--num-runners` accepts 1 and concurrent requests
156-
queue. Serving capacity is worker capacity, chosen by the launcher (each worker
157-
is its own process with its own weights, so N workers cost N × the weight memory)
158-
— an operator decision, not something the pool infers.
157+
unsupported sampling params are rejected, not ignored. **One worker process,
158+
serialized execution** (one in-flight request; concurrent requests queue).
159+
Session capacity is determined by the worker/engine — a single worker hosts many
160+
isolated sessions on one weight load — so `--num-runners` accepts 1; extra worker
161+
processes would each carry their own copy of the weights.
159162

160163
Cancellation is best-effort: a worker request runs to completion and is not
161164
interruptible mid-generation in V1, so `runner.stop()` means "the control plane

extension/llm/server/python/runner_pool.py

Lines changed: 0 additions & 132 deletions
This file was deleted.

extension/llm/server/python/server.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
no runtime pybind. Model execution lives in a separate C++ worker process
1515
(``text_llm_worker``) driven over JSONL via WorkerClient.
1616
17-
V1 is single-slot: one worker hosts one session, so concurrent requests queue.
18-
There is no prefix cache in V1 serving; caching, if it returns, lives inside the
19-
worker/session, not the control plane.
17+
One worker process, serialized execution (one in-flight request; concurrent
18+
requests queue). Session capacity is set by the worker/engine -- a single worker
19+
hosts many isolated sessions on one weight load; extra worker processes would
20+
duplicate the weights, so `--num-runners` accepts 1.
2021
2122
Example:
2223
python -m executorch.extension.llm.server.python.server \\
@@ -35,8 +36,8 @@
3536
from .chat_template import ChatTemplate
3637
from .errors import APIError
3738
from .protocol import ChatCompletionRequest, ModelCard, ModelList
38-
from .runner_pool import RunnerPool
3939
from .serving_chat import ServingChat
40+
from .session_runtime import SessionRuntime
4041
from .tool_parsers import HermesDetector
4142
from .worker_client import spawn_worker
4243

@@ -143,7 +144,8 @@ def main() -> None:
143144
"--num-runners",
144145
type=int,
145146
default=1,
146-
help="V1 supports 1 only (single-slot: one worker serves one session).",
147+
help="Worker processes. 1 only: one worker hosts many isolated sessions "
148+
"on a single weight load; more workers would duplicate the weights.",
147149
)
148150
p.add_argument(
149151
"--worker-bin",
@@ -158,7 +160,8 @@ def main() -> None:
158160

159161
if args.num_runners != 1:
160162
p.error(
161-
"V1 is single-slot: one worker serves one session; concurrent requests queue."
163+
"Only 1 worker process is supported (it hosts many isolated sessions "
164+
"on one weight load); more workers would duplicate the weights."
162165
)
163166

164167
default_template_kwargs = {"enable_thinking": False} if args.no_think else None
@@ -168,10 +171,10 @@ def main() -> None:
168171
default_template_kwargs=default_template_kwargs,
169172
allow_fallback=args.allow_chatml_fallback,
170173
)
171-
worker = _spawn(args) # one worker == one session (single-slot V1)
172-
pool = RunnerPool([worker])
174+
worker = _spawn(args) # one worker hosting many isolated sessions
175+
runtime = SessionRuntime(worker)
173176
serving = ServingChat(
174-
pool,
177+
runtime,
175178
template,
176179
args.model_id,
177180
max_context=args.max_context,
@@ -184,7 +187,7 @@ def main() -> None:
184187

185188
@app.on_event("shutdown")
186189
def _stop_worker():
187-
pool.close()
190+
runtime.close_worker()
188191

189192
import uvicorn # imported here so build_app() is usable without the ASGI server
190193

0 commit comments

Comments
 (0)