Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
221 commits
Select commit Hold shift + click to select a range
2430058
resources: publish snapshot hooks so a step can defer its publication
merceod Oct 7, 2026
91cae50
runner: publish_snapshot and publish_from_snapshots sweeps
merceod Oct 7, 2026
68d7c4d
kv: snapshot the publication per step and finish it on demand
merceod Oct 7, 2026
18d8e17
rope: snapshot the publication per step and finish it on demand
merceod Oct 7, 2026
e84b7c2
engine: finalize_batch snapshots the publication, materialize_publish…
merceod Oct 7, 2026
99593f1
worker: finish publications only for stopped rids and rids that send …
merceod Oct 7, 2026
258e27a
test: lazy publication
merceod Oct 7, 2026
b36967b
submodule_base: a stop buffer that is a row output shares the row clone
merceod Oct 7, 2026
52b171e
engine: collect_outputs fast path for row-only outputs
merceod Oct 7, 2026
9272246
test: collect_outputs fast path
merceod Oct 7, 2026
c05f8bb
micro_scheduler: check backlog readiness only up to a step's worth
merceod Oct 7, 2026
2191035
test: bounded backlog readiness checks
merceod Oct 7, 2026
f5a7327
resources: keys_prefix_chains says whether a prefix cache is open
merceod Oct 7, 2026
9e38073
kv: report an open prefix cache through keys_prefix_chains
merceod Oct 7, 2026
f1b0be5
runner: prefix_chain_keys, and extend_prefix_chains over a given key …
merceod Oct 7, 2026
5ef8de3
engine: skip the per-request prefix-chain sweep when no cache is open
merceod Oct 7, 2026
bdf055e
rust frames: RESULT_TOKENS, one frame per step for inline client values
merceod Oct 7, 2026
77d32ec
rust runtime: route an inline client signal without staging, refs or …
merceod Oct 7, 2026
77aab0c
graph runtime: RouteInput.inline_signal/inline_values and supports_in…
merceod Oct 7, 2026
db4def8
rust runtime wrapper: pass inline values, report supports_inline_emit
merceod Oct 7, 2026
95006f7
api_server: ResultTokens message type
merceod Oct 7, 2026
6661a71
wire: register result_tokens
merceod Oct 7, 2026
f6e5939
api_server: dispatch result_tokens frames to the data worker
merceod Oct 7, 2026
27322e4
data worker: detokenize inline tokens in arrival order, no read and n…
merceod Oct 7, 2026
810cf24
submodule_base: inline_client_signals hook
merceod Oct 7, 2026
1761cde
qwen3_5: the decode loop's client token rides inline
merceod Oct 7, 2026
02a6af9
engine: inline_client_signals passthrough
merceod Oct 7, 2026
d57681c
worker: hand the stop check's host tokens to the route as inline values
merceod Oct 7, 2026
21fb696
test: inline token path (wire, data worker, worker)
merceod Oct 7, 2026
a31c952
test: rust runtime inline emit
merceod Oct 7, 2026
b61b1b5
qwen3_5: decode capture buckets up to 128 rows
merceod Oct 7, 2026
51edd0c
api_server: wake the streaming handler on events instead of polling p…
merceod Oct 7, 2026
8d21448
test: a chunk from the message thread wakes the stream
merceod Oct 7, 2026
85c5367
submodule_base: step_is_input_free hook
merceod Oct 7, 2026
ba56be0
qwen3_5: decode steps declare from spans alone
merceod Oct 7, 2026
2ce4dac
engine: run the pre-plan's declared step for input-free walks
merceod Oct 7, 2026
6557266
docs: MSTAR_LAZY_PUBLISH
merceod Oct 7, 2026
080065c
worker: build and pre-plan N+2 while N+1 runs, between N's routing an…
merceod Oct 7, 2026
65ffc1a
docs: MSTAR_EARLY_SPEC
merceod Oct 7, 2026
7b21494
sampler: skip the static gather for a steady batch while other reques…
merceod Oct 8, 2026
6be4dd1
kv plan: build the paged indptr tensors through numpy
merceod Oct 8, 2026
ab3c97a
test: static gather skip with queued requests
merceod Oct 8, 2026
1025fd5
ci: list the new cpu tests
merceod Oct 8, 2026
7359e67
batched output: carry the row views the collect fast path built
merceod Oct 8, 2026
13e7943
engine: hand the row views to the collected output
merceod Oct 8, 2026
5a057b6
tensor store: batch the ownership maps, dtype index helper
merceod Oct 8, 2026
cb3d7e6
tensors: row store for a step whose outputs are row views of one batc…
merceod Oct 8, 2026
f19248a
worker: store decode row outputs through the row store
merceod Oct 8, 2026
129311f
test: merge_per_rid returns the row views
merceod Oct 8, 2026
c104c99
test: row store against the general path
merceod Oct 8, 2026
28d739d
ci: list the row store test
merceod Oct 8, 2026
760bde4
worker: build the stop check's per-rid host dicts only when read
merceod Oct 8, 2026
4e50fb7
engine: skip the unpack pass when nothing is packed
merceod Oct 8, 2026
9163255
test: lazy per-rid host dicts
merceod Oct 8, 2026
5e2b57c
ci: list the lazy per-rid test
merceod Oct 8, 2026
083cc0f
qwen3_5: MSTAR_QWEN35_DECODE_BUCKETS overrides the decode capture ladder
merceod Oct 8, 2026
5025706
docs: MSTAR_QWEN35_DECODE_BUCKETS
merceod Oct 8, 2026
a025fac
step context: constant-time padding and real-row tests
merceod Oct 8, 2026
b3a01b6
kv: batched publish snapshot under one lock, real-row test in the pag…
merceod Oct 8, 2026
91e4d28
resources: publish_snapshot_batch hook
merceod Oct 8, 2026
b2f60e6
runner: snapshot the publication per resource for the whole batch
merceod Oct 8, 2026
cb6bed1
sampler: one graph sampler per bucket and slot
merceod Oct 8, 2026
1dce205
test: step context row membership
merceod Oct 8, 2026
6cb5601
test: batched publish snapshot
merceod Oct 8, 2026
d2dcb38
test: sampler_for cache
merceod Oct 8, 2026
9f222e4
ci: list the step, snapshot and sampler tests
merceod Oct 8, 2026
fb04771
kv admit: skip the allocation call for rows whose pages already cover…
merceod Oct 8, 2026
a2d1155
engine: hoist the per-row constants out of prepare_inputs, look up th…
merceod Oct 8, 2026
dbbc572
arena: batched cleanup of collectable tensors
merceod Oct 8, 2026
50baf43
worker: send only the request entries that carry counts or stream con…
merceod Oct 8, 2026
5404251
engine: direct properties for the forward-pass wrapper's hot attributes
merceod Oct 8, 2026
bcfe673
test: batched stream consumption report
merceod Oct 8, 2026
75f5ea4
ci: list the send prep test
merceod Oct 8, 2026
c72d6af
kv commit: per-row constants hoisted, index offer only with a chain
merceod Oct 8, 2026
3a31d8b
api_server: wake the streaming handlers once per message pass
merceod Oct 8, 2026
b861dcf
test: batched handler wake-up
merceod Oct 8, 2026
f5f5303
rust runtime: env-gated timing of send_outputs (body vs GIL reacquire)
merceod Oct 8, 2026
6de4423
worker: MSTAR_SPEC_YIELD_HOLD_MS holds the decode loop's yield so arr…
merceod Oct 8, 2026
2b422b9
docs: MSTAR_SPEC_YIELD_HOLD_MS
merceod Oct 8, 2026
720587d
rust runtime: flat export of the dynamic loop iterations
merceod Oct 8, 2026
d47c22f
graph runtime: update_dynamic_loop_iters hook
merceod Oct 8, 2026
880cdbe
rust runtime wrapper: refresh loop iterations from the flat export
merceod Oct 8, 2026
cb0b9ce
worker: refresh loop iterations through the runtime hook
merceod Oct 8, 2026
aa5c467
test: update_dynamic_loop_iters matches the per-rid form
merceod Oct 8, 2026
92bed84
kv: batched prefix-chain extension off the stop check's host rows
merceod Oct 8, 2026
cb338d8
runner: batched chain extension; sweep resources that override the ho…
merceod Oct 8, 2026
da6df5b
resources: document when the chain hook is swept
merceod Oct 8, 2026
61ea439
engine: extend prefix chains from the host rows
merceod Oct 8, 2026
f66cb9b
worker: hand the host rows to the prefix-chain extension
merceod Oct 8, 2026
a490f5b
test: batched prefix-chain extension
merceod Oct 8, 2026
b369e75
ci: list the prefix batch test
merceod Oct 8, 2026
60c0ab2
engine: read the keyed walks leniently in prepare_inputs
merceod Oct 8, 2026
d35c66b
zmq communicator: non-blocking try_send
merceod Oct 8, 2026
22278c5
rust runtime: send frames with the GIL held, release it only for a pe…
merceod Oct 8, 2026
04a8f36
docs: MSTAR_RUST_SEND_TIMING
merceod Oct 8, 2026
5db372f
data worker: sort the import block
merceod Oct 8, 2026
5912708
rust runtime: the inline signal keys of a route input are optional
merceod Oct 8, 2026
4406b62
test: row store tests skip without the columnar tensor store
merceod Oct 8, 2026
4a527fa
event wakeup: a public signal for producers
merceod Oct 8, 2026
5fea346
data worker: block on the communicator's poll when idle instead of a …
merceod Oct 8, 2026
79cc69a
test: the data worker's idle wait and its wake-ups
merceod Oct 8, 2026
b261d9b
ci: list the data worker wake-up test
merceod Oct 8, 2026
bf6762c
sampler buffers: a slot-addressed last-token master written by the ca…
merceod Oct 8, 2026
9e63ef0
sampler resource: scatter the step's tokens at commit, read them back…
merceod Oct 8, 2026
bb28b08
submodule base: the device loop-back hook and its knob
merceod Oct 8, 2026
970c8ce
qwen3.5: decode input ids off the sampler's slot master
merceod Oct 8, 2026
954bace
engine: expose the node's device loop-back signals
merceod Oct 8, 2026
cf9b4f8
worker: no store, hold or tensors for a step kept on the device
merceod Oct 8, 2026
05ebf8e
docs: MSTAR_DEVICE_LOOPBACK
merceod Oct 8, 2026
5ea711f
test: the sampler's last-token master
merceod Oct 8, 2026
21f920a
test: qwen3.5 decode inputs from the sampler master
merceod Oct 8, 2026
4f0fadb
test: the worker's device loop-back decision
merceod Oct 8, 2026
d57c15d
test: a loop-back routed with no tensor keeps the loop going
merceod Oct 8, 2026
1a15e53
ci: list the device loop-back tests
merceod Oct 8, 2026
874940e
worker: early speculation on TP leaders, committing only with a head …
merceod Oct 8, 2026
0abe28b
docs: MSTAR_EARLY_SPEC on tensor-parallel nodes
merceod Oct 8, 2026
b794f72
test: a TP leader's early build takes each step's decision once
merceod Oct 8, 2026
779b9f3
ci: list the TP early speculation test
merceod Oct 8, 2026
e44975b
worker: the spec splice runs on the main thread, not the plan thread
merceod Oct 8, 2026
f5ec4d3
utils: freeze the set-up heap after a process starts
merceod Oct 8, 2026
f91a2c2
conductor: freeze the set-up heap once the workers are ready
merceod Oct 8, 2026
e2b139c
api server: freeze the set-up heap once the workers are ready, one da…
merceod Oct 8, 2026
201e484
data worker: a token put without a wake, and a wake for the frame
merceod Oct 8, 2026
13e4251
docs: MSTAR_GC_FREEZE
merceod Oct 8, 2026
2946801
test: the heap freeze and the per-frame wake
merceod Oct 8, 2026
7b2fe58
ci: list the gc freeze test
merceod Oct 8, 2026
a8a1e76
api server: the gc freeze import belongs with the module imports
merceod Oct 8, 2026
79b076e
conductor: import block sorted
merceod Oct 8, 2026
31dc82c
utils: the device loop-back knob in one place
merceod Oct 8, 2026
6422ea8
submodule base: take the loop-back knob from utils
merceod Oct 8, 2026
0628a7d
sampler resource: persist the last token only with the loop-back on, …
merceod Oct 8, 2026
2538034
qwen3.5: time the loop-back gather
merceod Oct 8, 2026
a111f51
qwen3.5: text prefill graph buckets up to 8192 tokens, with a knob
merceod Oct 9, 2026
6471b57
docs: MSTAR_QWEN35_PREFILL_TOKEN_BUCKETS
merceod Oct 9, 2026
809af26
test: the qwen3.5 bucket ladders come from the environment
merceod Oct 9, 2026
c553dee
ci: list the bucket knob test
merceod Oct 9, 2026
b00e24c
utils: tokenize a long prompt as parallel chunks, identical ids
merceod Oct 9, 2026
d19919f
qwen3.5: encode the rendered prompt through the chunked path
merceod Oct 9, 2026
723e31f
test: chunked tokenization cuts and identity
merceod Oct 9, 2026
65f24c9
ci: list the chunked tokenization test
merceod Oct 9, 2026
9b2415f
utils: wrap the chunked tokenizer signatures
merceod Oct 9, 2026
148605f
api: orjson for the SSE events, json fallback
merceod Oct 9, 2026
53ea6e9
api: iter_result_chunk_batches, one list per drain
merceod Oct 9, 2026
ecc6d44
api: chat stream sends a batch of text events as one frame
merceod Oct 9, 2026
b1a8dc3
test: result chunk batches
merceod Oct 9, 2026
931ac12
test: coalesced chat stream frames, sse line
merceod Oct 9, 2026
0b570c5
kv: host lengths iterate as numpy scalars for FlashInfer's max
merceod Oct 9, 2026
9cd55c7
test: kv plan host lengths
merceod Oct 9, 2026
bd5bf7c
runner: admit skips the resources pre_admit covered
merceod Oct 9, 2026
e0c3337
engine: phase samples for the pre-plan's declare and admit
merceod Oct 9, 2026
08141f9
worker: postprocess stamps that add up
merceod Oct 9, 2026
2c8de51
test: admit after pre_admit
merceod Oct 9, 2026
ef7eb66
submodule: host_rows on the batched output
merceod Oct 9, 2026
f80c710
worker: stage the stop rows' D2H on the gpu thread behind the step
merceod Oct 9, 2026
7d9eb7e
test: staged host rows
merceod Oct 9, 2026
56dcfa8
submodule: rows_only on the batched output, hook docs
merceod Oct 9, 2026
6f2669e
engine: rows-only collect on a device loop-back step, no tail
merceod Oct 9, 2026
6798a40
worker: rows-only steps, splice the loop-back signal as present
merceod Oct 9, 2026
a98f888
test: rows-only collect
merceod Oct 9, 2026
c5ded39
test: stub carries rows_only
merceod Oct 9, 2026
cdba6fd
test: rows-only splice
merceod Oct 9, 2026
bd75d9d
test: stub carries output_signals
merceod Oct 9, 2026
633a284
engine: no clone on a rows-only step
merceod Oct 9, 2026
627fc0e
sampler: memoized per-step views for the commit scatters
merceod Oct 9, 2026
727d3bd
test: rows-only collect takes no clone
merceod Oct 9, 2026
0014739
worker: skip the stream polls of a speculation build when no request …
merceod Oct 9, 2026
30eee5a
test: stream polls skipped only without buffers
merceod Oct 9, 2026
a717532
submodule: uniform_row_inputs hook
merceod Oct 9, 2026
4243dad
qwen3_5: the loop-back decode row is uniform
merceod Oct 9, 2026
912b2e0
engine: prepare a uniform walk with one object
merceod Oct 9, 2026
9191dcc
test: uniform loop-back row
merceod Oct 9, 2026
1e8efa0
test: engine uniform inputs
merceod Oct 9, 2026
9db4d7a
qwen3_5: a captured decode step gathers its tokens and rotary tables …
merceod Oct 9, 2026
d16a5b6
test: captured decode stages no input
merceod Oct 9, 2026
5adfacc
knobs: MSTAR_LAUNCH_SIGNAL_AFTER_COMMIT
merceod Oct 9, 2026
3932a49
worker: _exec_step, launch signal after commit behind the knob
merceod Oct 9, 2026
999eab1
docs: MSTAR_LAUNCH_SIGNAL_AFTER_COMMIT
merceod Oct 9, 2026
f6e0d22
test: launch signal placement
merceod Oct 9, 2026
b9412aa
qwen3_5: cheaper batched stop check
merceod Oct 9, 2026
6f83e10
kv: the chain extension looks a request's label up once
merceod Oct 9, 2026
e14a743
test: stop check off a reordered batch
merceod Oct 9, 2026
6799af3
api: inline tokens as ids for models that decode from ids
merceod Oct 9, 2026
44e7ba7
qwen3_5: inline postprocess takes ids
merceod Oct 9, 2026
2d91a8d
test: inline value as a list
merceod Oct 9, 2026
85bc776
knobs: MSTAR_SAMPLER_INGRAPH_SCATTER, MSTAR_SAMPLER_SLOTS
merceod Oct 9, 2026
38c240e
sampler: offset gather and the two commit scatters inside the graph, …
merceod Oct 9, 2026
8f6e60e
sampler: commit skips the scatters the graph did
merceod Oct 9, 2026
8bedee4
docs: MSTAR_SAMPLER_INGRAPH_SCATTER
merceod Oct 9, 2026
9cc1fdd
test: in-graph scatter rows and the trash row
merceod Oct 9, 2026
ee4bdbd
test: offset reset runs before the in-graph gather
merceod Oct 9, 2026
9500c8f
Revert "test: inline value as a list"
merceod Oct 9, 2026
01a929e
Revert "qwen3_5: inline postprocess takes ids"
merceod Oct 9, 2026
7d3b9fe
Revert "api: inline tokens as ids for models that decode from ids"
merceod Oct 9, 2026
fbc37e1
utils: the chunked encode turns the tokenizer crate's parallelism on
merceod Oct 9, 2026
2b13bfa
docs: MSTAR_TOKENIZE_PARALLEL
merceod Oct 9, 2026
d87e034
test: chunked encode flips TOKENIZERS_PARALLELISM
merceod Oct 9, 2026
b0e3cc8
knobs: MSTAR_TP_EARLY_SPEC
merceod Oct 9, 2026
3363443
worker: a parallel node builds early only with the knob
merceod Oct 9, 2026
1428792
docs: MSTAR_TP_EARLY_SPEC
merceod Oct 9, 2026
bb75844
test: tp early build knob
merceod Oct 9, 2026
a66c7ad
Revert "test: captured decode stages no input"
merceod Oct 9, 2026
df64756
Revert "qwen3_5: a captured decode step gathers its tokens and rotary…
merceod Oct 9, 2026
e3c54f5
engine: a reset pre-plan drops its kept step
merceod Oct 12, 2026
f6274b1
test: a partly dropped batch does not keep its pre-planned step
merceod Oct 12, 2026
eef4b85
kv: a snapshot of a stream reset since publishes the stream as it stands
merceod Oct 12, 2026
57ebad9
test: publish from a snapshot after a reset
merceod Oct 12, 2026
b3e3b55
worker: drop the spec yield hold, note why two slots suffice, note th…
merceod Oct 12, 2026
734972e
docs: MSTAR_SPEC_YIELD_HOLD_MS removed, MSTAR_DECODE_BUCKETS added
merceod Oct 12, 2026
5b54fc7
api: APIServerMessageBody names the message body union
merceod Oct 12, 2026
5838168
tensor store: ColumnarTensorInfo.add_row
merceod Oct 12, 2026
37f9613
tensors: the row fast path fills the columns through add_row
merceod Oct 12, 2026
3a814eb
qwen3_5: decode buckets from MSTAR_DECODE_BUCKETS, the model knob ove…
merceod Oct 12, 2026
18ea22e
merge perf/host-overhead review fixes into perf/host-overhead-2
merceod Oct 12, 2026
d2137a5
test: the fairness settings without the hold
merceod Oct 12, 2026
41bc8e2
race on resizing of master buffer could cause loss of one batch of de…
stephen-dwq Oct 13, 2026
2aaff1a
python graph would emit no device-loopback decode tokens to client be…
stephen-dwq Oct 13, 2026
d5706e5
lock to prevent race on host-side tensors that could lose request con…
stephen-dwq Oct 13, 2026
aa76f1d
Merge origin/main into perf/host-overhead-2
NSagan271 Oct 14, 2026
b48904d
Review comments: remove env vars for MSTAR_GC_FREEZE (always on), and…
NSagan271 Oct 14, 2026
da95226
ruff
NSagan271 Oct 14, 2026
4a08df8
fix rows_only for a request that runs _exec_single but still doesn't …
NSagan271 Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/environment_variables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,13 @@ Read by the ``mstar-server`` binary and its bridge
* - ``MSTAR_MAX_BODY_MB``
- ``128``
- Request body limit (multipart uploads included).
* - ``MSTAR_TOKENIZE_PARALLEL``
- ``1``
- When a long prompt is encoded as parallel chunks, set
``TOKENIZERS_PARALLELISM=true`` for the process first: the Rust
tokenizer only uses its thread pool while that variable reads true,
and serving environments often export it false. ``0`` leaves the
variable as it is (the chunked encode then runs serially).
* - ``MSTAR_TOKENIZER``
- unset
- Path to a HuggingFace ``tokenizer.json`` enabling frontend
Expand Down Expand Up @@ -315,6 +322,32 @@ Worker scheduling
- ``1024``
- Cap on back-to-back speculative steps before the leader yields to
other ready work.
* - ``MSTAR_QWEN35_PREFILL_TOKEN_BUCKETS``
- ``32,64,128,256,512,1024,2048,4096,8192``
- Total tokens of a packed Qwen3.5 text-prefill step that get a captured
graph (one per bucket and batch size). A prompt past the top bucket
prefills eagerly, which is launch-bound: an eager 8k prefill on 0.8B
spends 78 ms of wall time on 36 ms of kernels. The default now reaches
8192; narrow it to save graph memory and capture time, or extend it.
* - ``MSTAR_DEVICE_LOOPBACK``
- ``1``
- Whether a decode node may keep its loop-back token on the device:
the sampler writes each request's last token to a slot-addressed
master and the next step reads its input ids from it, so the worker
routes the signal with no tensor (no per-request store, hold or
cleanup per step). Only nodes that opt in (Qwen3.5 decode) and only
when the sampler has graph buffers; ``0`` keeps every node on the
per-request tensor path.
* - ``MSTAR_SAMPLER_INGRAPH_SCATTER``
- ``0``
- The captured sampler gathers its RNG offsets from the slot masters
and scatters the advanced offsets and the sampled tokens back inside
the graph, so a replayed decode step issues none of those launches on
the gpu thread afterwards (each one is a GIL release the main thread's
postprocess wins). Padding rows then address a trash master row and
the masters are allocated once for ``MSTAR_SAMPLER_SLOTS`` concurrent
requests (default 4096, a hard cap with this on). Only with the device
loop-back on. An experiment knob; measure before turning it on.
* - ``MSTAR_RUST_SEND_TIMING``
- unset
- ``1`` makes the Rust runtime print, every 500 calls, how long the
Expand Down
61 changes: 58 additions & 3 deletions mstar/api_server/data_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
ResultTensors,
)
from mstar.communication.communicator import BaseCommunicator, CommProtocol, make_communicator
from mstar.communication.event import EventWakeup
from mstar.communication.tensors import NameToTensorList, create_tensor_communication_manager
from mstar.engine.resources.kv.config import KVSpec, PagedKVConfig
from mstar.engine.resources.kv.keys import chain
Expand Down Expand Up @@ -138,6 +139,8 @@ def __init__(
self.output_queue = queue.Queue()
self.profile_queue = queue.Queue()
self.stop_event = threading.Event()
# Wakes the thread's idle wait: every producer below signals it.
self.wakeup = EventWakeup()

self.per_request_reading_tensors = {}
self.output_loop_idxs: dict[str, NameToLoopIndices] = {}
Expand Down Expand Up @@ -174,6 +177,7 @@ def __init__(
reads_done_queue=self.reads_done_queue,
discard_tensor_queue=self.discard_tensor_queue,
stop_event=self.stop_event,
wakeup=self.wakeup,
communicator=self.communicator,
tensor_manager=self.tensor_manager,
model=model,
Expand All @@ -184,10 +188,18 @@ def __init__(
)
self.thread.start()

def _signal(self):
"""End the thread's idle wait: new work is queued. getattr: tests
build this facade without its constructor."""
wakeup = getattr(self, "wakeup", None)
if wakeup is not None:
wakeup.signal()

def new_request(self, input: PreprocessInput):
self.output_loop_idxs[input.request_id] = {}
self.per_request_reading_tensors[input.request_id] = 0
self.request_input_queue.put(input)
self._signal()

def abort_request(self, request_id: str):
# Forward the abort to the conductor and begin draining our own reads
Expand All @@ -196,6 +208,7 @@ def abort_request(self, request_id: str):
# REMOVE_REQUEST, so a worker still reading them can't be unlinked
# out from under it. Drop only the main-thread bookkeeping here.
self.abort_request_queue.put(request_id)
self._signal()
self.output_loop_idxs.pop(request_id, None)
self.per_request_reading_tensors.pop(request_id, None)

Expand All @@ -207,6 +220,7 @@ def finished_reading(self, request_id: str, drained: bool = True):
when delivery was abandoned (TTL, client gone) and the ACK must wait
for any in-flight read."""
self.reads_done_queue.put((request_id, drained))
self._signal()
self.output_loop_idxs.pop(request_id, None)
self.per_request_reading_tensors.pop(request_id, None)

Expand All @@ -229,13 +243,17 @@ def new_result_tensors(self, input: ResultTensors):
input.request_id, self.per_request_reading_tensors[input.request_id]
)
self.result_tensor_input_queue.put(input)
self._signal()

def new_result_token(
self, request_id: str, value: int,
loop_indices: NestedLoopIndices | None, signal: str, modality: str,
wake: bool = True,
):
"""A value that rode inline: same accounting as a result tensor, no
transport read and nothing to ack."""
transport read and nothing to ack. ``wake=False`` queues without
ending the thread's idle wait: a frame's caller hands over every
request's value first and calls ``wake`` once."""
if request_id not in self.output_loop_idxs:
logger.debug("Late token for cleaned-up request %s dropped", request_id)
return
Expand All @@ -247,6 +265,12 @@ def new_result_token(
self.result_tensor_input_queue.put(
InlineToken(request_id, value, loop_indices, modality)
)
if wake:
self._signal()

def wake(self):
"""End the thread's idle wait after a batch of ``wake=False`` puts."""
self._signal()

def discard_result_tensors(self, input: ResultTensors):
"""Ack and drop result tensors for an already-removed request.
Expand All @@ -255,6 +279,7 @@ def discard_result_tensors(self, input: ResultTensors):
producing worker gets its TENSOR_RECEIVED ack and frees the buffers.
"""
self.discard_tensor_queue.put(input)
self._signal()

def has_pending_tensors(self, request_id: str):
return self.per_request_reading_tensors.get(request_id, 0) > 0
Expand Down Expand Up @@ -324,11 +349,13 @@ def get_rx_info(self, request_id: str) -> list[RxInfo]:

def cleanup_request(self, request_id: str):
self.cleanup_request_queue.put(request_id)
self._signal()
self.output_loop_idxs.pop(request_id, None)
self.per_request_reading_tensors.pop(request_id, None)

def shutdown(self):
self.stop_event.set()
self._signal()
if self.thread.is_alive():
self.thread.join()

Expand Down Expand Up @@ -384,6 +411,7 @@ def __init__(
enable_prof: bool=False,
model_config: dict | None = None,
enable_nvtx: bool=False,
wakeup: EventWakeup | None = None,
):
# keying a stream needs the deployment's page size as well as the
# model's declaration, so a worker built without a config keys no stream
Expand Down Expand Up @@ -428,6 +456,12 @@ def __init__(
# Owned by PreprocessWorker (main thread); used only from this thread.
self.communicator = communicator
self.tensor_manager = tensor_manager
# Set by the producers (queue puts, shutdown) and by finished tensor
# reads; run() blocks on the communicator's poll with it instead of
# sleeping and sweeping the queues. None keeps the sleep (tests built
# with a bare communicator).
self.wakeup = wakeup
self.idle_wait_ms = 20

def _cleanup_request_state(self, request_id: str, *, force: bool = False) -> None:
"""Release transport and postprocessing state owned by this thread.
Expand Down Expand Up @@ -677,10 +711,14 @@ def _read_result_tensor(
self, result: ResultTensors
):
result.graph_edge.name = f"{result.modality}_output"
self.tensor_manager.start_read_tensors(
futures = self.tensor_manager.start_read_tensors(
request_id=result.request_id,
graph_edges=[result.graph_edge],
)
wakeup = getattr(self, "wakeup", None)
if wakeup is not None and futures:
# a finished read ends the idle wait, so the chunk goes out at once
wakeup.register_futures(futures)
if result.request_id not in self.tensor_uuid_to_metadata_per_request:
self.tensor_uuid_to_metadata_per_request[result.request_id] = {}
state = self.request_output_state.setdefault(
Expand Down Expand Up @@ -930,11 +968,28 @@ def _hard_cleanup(self, request_id: str) -> None:
self.tensor_manager.force_cleanup_request(request_id)
self._drop_request_state(request_id)

def _idle_wait(self):
"""How a pass that found nothing waits: on the communicator's poll,
ended by a message, a producer's signal or a finished read, with
``idle_wait_ms`` as the backstop; or a 1 ms sleep when the thread has
no wakeup or its communicator cannot poll one."""
# getattr: teardown tests build a thread without its constructor
wakeup = getattr(self, "wakeup", None)
comm = getattr(self, "communicator", None)
register = getattr(comm, "register_event_for_poll", None)
wait = getattr(comm, "wait_for_work", None)
if wakeup is None or not callable(register) or not callable(wait):
return lambda: time.sleep(0.001)
register(wakeup)
idle_ms = getattr(self, "idle_wait_ms", 20)
return lambda: wait(idle_ms)

def run(self):
# A thread told to stop before it starts (teardown tests build one with
# only its queues) has nothing to warm up.
if not self.stop_event.is_set():
warm_up_model(getattr(self, "model", None))
idle_wait = self._idle_wait()
while not self.stop_event.is_set():
did_work = False
try:
Expand Down Expand Up @@ -1018,7 +1073,7 @@ def run(self):
logger.exception("PreprocessWorkerThread error")

if not did_work:
time.sleep(0.001)
idle_wait()

# Stopping, and nothing will send the RemoveRequest for what is still
# tracked (the conductor is gone or going), so drop it here rather than
Expand Down
55 changes: 42 additions & 13 deletions mstar/api_server/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from mstar.profile.format import OutputInfo, RequestProfile, RequestTiming
from mstar.utils import profiler
from mstar.utils.exitcode import describe_exitcode
from mstar.utils.gc_freeze import freeze_after_setup
from mstar.utils.logging_config import quiet_noisy_loggers
from mstar.utils.orphan import watch_parent

Expand Down Expand Up @@ -366,6 +367,9 @@ def finalize_setup(self) -> None:
and message.message_type == "setup_done"
):
logger.info("All workers ready")
# the set-up heap is complete: keep gen-2 collections off
# it (measured 200 ms per pass on this process under load)
freeze_after_setup("API server")
self._msg_thread.start()
return
logger.warning(
Expand Down Expand Up @@ -578,18 +582,24 @@ def _process_messages(self) -> None:
# One frame for the step: every request's value.
# A request that is gone needs nothing acked back.
body = message.body
queued = False
with self.request_lock:
for i, rid in enumerate(body.request_ids):
if rid in self.pending_requests:
# one wake-up for the frame, after every
# request's value is queued
self.preprocess_worker.new_result_token(
rid, body.values[i],
body.loop_indices[i],
body.signal, body.modality,
body.signal, body.modality, wake=False,
)
queued = True
elif rid not in self.recently_completed:
logger.warning(
"Token for unknown request %s dropped", rid,
)
if queued:
self.preprocess_worker.wake()
continue

rid = message.body.request_id
Expand Down Expand Up @@ -733,14 +743,31 @@ def _process_messages(self) -> None:
# ----------------------------------------------------------

async def iter_result_chunks(self, request_id: str):
"""Yield raw :class:`ResultChunk` objects as they arrive.
"""Yield raw :class:`ResultChunk` objects as they arrive, one at a time.

Shared source for both output surfaces: ``/generate`` formats each
chunk as NDJSON (via :meth:`async_stream_results`), while the
OpenAI-compatible endpoints translate the same chunks into SSE. The
per-request timeout, incremental drain, and final flush behave exactly
as before — only the yielded type changed (``ResultChunk`` instead of a
pre-serialized line).
OpenAI-compatible endpoints translate the same chunks into SSE. This
is a flattening wrapper over :meth:`iter_result_chunk_batches`, which
hands out the chunks the way they arrive (several per wake under load)
and owns the timeout, the drain and the abort on an early exit.
"""
batches = self.iter_result_chunk_batches(request_id)
try:
async for batch in batches:
for chunk in batch:
yield chunk
finally:
await batches.aclose()

async def iter_result_chunk_batches(self, request_id: str):
"""Yield the :class:`ResultChunk` objects as they arrive, as one list
per drain: everything the message thread appended since the last
wake. Under load several tokens of a request are often waiting when
the handler runs, and sending them as one body frame costs one write
instead of one per token; when the loop keeps up the lists are of
length one and nothing is delayed. The final flush and an in-band
error come as the last list.
"""
start = time.time()
finished = False
Expand Down Expand Up @@ -773,10 +800,11 @@ async def iter_result_chunks(self, request_id: str):
else:
done = True

for chunk in new_chunks:
if new_chunks:
if self.enable_nvtx:
profiler.mark("apiserver.chunk_available")
yield chunk
for _ in new_chunks:
profiler.mark("apiserver.chunk_available")
yield new_chunks

if done:
logger.info("Async stream results received finish for %s", request_id)
Expand All @@ -792,19 +820,20 @@ async def iter_result_chunks(self, request_id: str):
# lock; the popped request is no longer shared with other threads.
if finished_req is not None:
self._finalize_profile(finished_req)
for chunk in remaining:
yield chunk
tail = list(remaining)
# A request can fail after the stream is already open
# (preprocess error, result-delivery timeout); the HTTP
# status is committed by then, so the error must travel
# in-band as the final chunk.
if finished_req is not None and finished_req.error is not None:
yield ResultChunk(
tail.append(ResultChunk(
request_id=request_id,
modality="error",
data=str(finished_req.error).encode("utf-8"),
metadata={"status": finished_req.error_status},
)
))
if tail:
yield tail
finished = True
break

Expand Down
13 changes: 12 additions & 1 deletion mstar/api_server/openai/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@ def rid(prefix: str) -> str:


def sse(obj: dict) -> str:
return f"data: {json.dumps(obj)}\n\n"
return f"data: {_dumps(obj)}\n\n"


try:
import orjson

def _dumps(obj: dict) -> str:
# ~5x json.dumps; one event per token at tens of thousands a second
return orjson.dumps(obj).decode()
except ImportError: # pragma: no cover
def _dumps(obj: dict) -> str:
return json.dumps(obj)


def error_type(status: int) -> str:
Expand Down
Loading
Loading