smg-grpc-servicer
gRPC servicer implementations for LLM inference engines. Supports vLLM, MLX, TokenSpeed, and SGLang.
Installation
For vLLM:
pip install smg-grpc-servicer[vllm]
For MLX:
pip install smg-grpc-servicer[mlx]
For TokenSpeed, install the TokenSpeed runtime first, then install the servicer bridge:
pip install smg-grpc-servicer
For SGLang:
pip install smg-grpc-servicer[sglang]
Usage
vLLM
vllm serve meta-llama/Llama-2-7b-hf --grpc
Without --kv-events-config, the servicer turns vLLM's ZMQ KV-event publisher
on by itself, under either implementation, before the engine is built, at a
port found free on the host rather than vLLM's fixed default (one per
data-parallel rank), so SubscribeKvEvents relays the events a cache-aware
router routes by and workers that share a host do not collide; an explicit
configuration is kept as given, its endpoint and off included, and
SMG_VLLM_SERVICER_KV_EVENTS=0 leaves the publisher off.
Worker-side multimodal processing (media refs)
By default the smg router fetches and preprocesses images itself and sends
pixel tensors. A vLLM gRPC worker can instead accept media references (URLs)
and run vLLM's own multimodal processor (or, on the Rust request path, smg's
own pipeline with --mm-processor smg; see below):
vllm serve Qwen/Qwen3-VL-8B-Instruct --grpc --mm-processor inprocess \
--allowed-media-domains example.com
The --mm-* flags come from a vLLM launcher that knows them (it hands an
MmSettings to VllmEngineServicer), or from this package's general plugin,
which defines them on a gRPC launcher's parser that lacks them: vllm serve --grpc and the stock python -m vllm.entrypoints.grpc_server (there the
values also reach a servicer built without the namespace, through the
environment). A flag left out falls back to the matching SMG_VLLM_MM_*
variable. Legacy variables log a deprecation line on a launcher that has the
flag; the TurboJPEG policy and download timeout variables remain supported
fallbacks and never log a deprecation warning. A launcher without the flags
(no plugins loaded, a servicer built on its own) takes the variable as its
documented setting and logs nothing. The startup
line VllmEngineServicer initialized (mm_processor=inprocess, source=flag) and
the mm_processor_source label name where the value came from.
A model family whose reference image processor raises a short side below a
floor before its own resize (MiniMax-M3: 112 px) gets the same raise on the
worker: inprocess and the sidecar raise such an image to the floor, bicubic,
before the engine's processor sizes it, as the router's own pipeline does, so
a tiny image costs the same visual tokens whichever component preprocessed it
(video frames are not raised, as there). An engine started with
--mm-processor-kwargs '{"do_resize": false}' raises nothing, as the pipeline
raises nothing then.
The worker then advertises mm_processor=inprocess and mm_media_ref_schemes
through GetServerInfo; a router with media-reference support forwards
media_refs only to workers that advertise, and a router without it ignores the
labels and keeps sending preprocessed tensors. Whatever the processor mode, a
multimodal engine also advertises its resolved --limit-mm-per-prompt as
mm_item_limits (image=8,video=2), so a router that preprocesses media
itself holds requests to the engine's limits, and its encoder budget as
mm_encoder_budget (the budget of N tokens of the engine's encoder-cache
start-up line, derived at launch with vLLM's own helper): the most feature
tokens one media item may carry and still be scheduled, so the router refuses
a larger item (a long video sampled at a high rate) with a 400 that names the
item's size and the budget, instead of sending the engine an item it admits
and never schedules. vLLM's --allowed-media-domains,
--allowed-local-media-path, --media-io-kwargs, --limit-mm-per-prompt and
VLLM_*_FETCH_TIMEOUT govern fetching on the worker; without
--allowed-media-domains the worker fetches from any public host the router
forwards. A media URL that names or resolves to a loopback, link-local or
private address is refused on the worker as on the router, redirects included,
unless SMG_MM_ALLOW_PRIVATE_MEDIA_HOSTS=1 is set for a deployment whose media
origin is internal.
Related knobs: --mm-max-inflight (SMG_VLLM_MM_MAX_INFLIGHT, default 64)
bounds concurrent media jobs; --mm-max-items (SMG_VLLM_MM_MAX_ITEMS, unset
by default) overrides the model's per-modality reference limits;
--mm-max-item-bytes (SMG_VLLM_MM_MAX_ITEM_BYTES, default 32 MiB) caps inline
data: payloads; SMG_VLLM_MM_MAX_VIDEO_FRAMES (env only for now; default 0,
meaning vLLM's own --media-io-kwargs decide) caps the frames a video is
sampled to, so long clips stay bounded. All nine flag-backed settings are
validated when the servicer starts, whatever the processor mode, so a stale
unreadable value fails loudly. Two layers bound an inline item: the router
refuses a data: item above its own per-kind input bound
(SMG_IMAGE_MAX_INPUT_BYTES, SMG_VIDEO_MAX_INPUT_BYTES,
SMG_AUDIO_MAX_INPUT_BYTES; default 256 MiB each) with a 400
media_ref_too_large before the worker sees it, and the worker refuses one
above --mm-max-item-bytes with a message that names the worker as the flag's
owner, which the router relays as a 400 start_generation_failed. With the
defaults a 50 MB item clears the router and stops at the worker's 32 MiB, so
that flag is the one to raise; the router's --max-payload-size (default
512 MiB) must also hold the whole request body.
Media URL download budgets can be set with --mm-fetch-timeout-ms (images
and audio, default 10000) and --mm-video-fetch-timeout-ms (video,
default 30000). Both take positive integer milliseconds and cover the
fetch from connection through the last byte, not decoding or inference.
Set them on the process that downloads the media:
- Router preprocessing (shared by vLLM, SGLang and TokenSpeed): the Rust
and Python router CLIs accept both flags, with environment fallbacks
SMG_MM_FETCH_TIMEOUT_MSandSMG_MM_VIDEO_FETCH_TIMEOUT_MS. - vLLM Rust worker,
--servicer-impl rust --mm-processor smg: the same flags useSMG_VLLM_MM_FETCH_TIMEOUT_MSandSMG_VLLM_MM_VIDEO_FETCH_TIMEOUT_MSas environment fallbacks.
An explicit flag takes precedence over its environment variable. Unset
values retain the defaults above. These settings do not change downloads
performed by vLLM's own inprocess or redis processors; configure those
through the engine's media loader settings. SGLang and TokenSpeed currently
use the Router's media pipeline, so set the budgets on the Router for them.
A fetch that fails is answered under media_fetch_failed by whose fault it
is, on the Router's pipeline and the vLLM Rust worker's alike: a reference the
request got wrong (a URL that cannot be parsed or resolved, a host that refuses
the connection or answers 4xx, a payload that is not the media it claims to
be) is a 400; a host that does not deliver within the budget, answers 5xx or
breaks the transfer is a 502, so a client knows whether to retry. Neither
counts against the worker's circuit breaker, and the Router does not replay
either on another worker, which would meet the same host.
On the router side, --mm-processing selects auto (default: forward when
every registered worker of the model advertises mm_processor and the model's
spec opts in, or every one of those workers runs the router's own pipeline,
mm_processor=smg, which expands any spec's image or video anchor itself), router (always
preprocess) or worker (strict: 400 when a request cannot be forwarded, and
503 no_media_ref_capable_worker for a model none of whose registered workers
advertises mm_processor, answered before any worker is chosen, except a model
with an encode worker among them: on the encode-prefill-decode pipeline media
references never travel, a request that would forward them is answered its own
400 multimodal_not_supported, and such a model is not named under
models_without_media_processing either; a family
without the opt-in forwards when every media-processing worker of the model
runs smg, and its requests are placed on smg workers only, so a worker on
the engine's own processor never receives an anchor it cannot expand; while a
model whose workers report a vision encoder has no worker advertising
mm_processor, the ready body of /readiness names the model under
models_without_media_processing, the pod staying ready for its text requests
and its other models, and each vision worker without a media processor is
warned about once at registration); the outcome is counted in
smg_mm_processing_total{model,mode,reason} (pipeline_workers and
auto_pipeline name the forwarded-by-the-pipeline case), and the startup line
multimodal processing mode names the value and its source (flag, env or
default). SMG_MM_PROCESSING is the deprecated env fallback: it applies only
when the flag is absent, logs a deprecation line, and goes away in the next
minor release. Any other value stops the router at startup instead of quietly
reverting to auto. On the worker path the
router never expands placeholders, so routing decisions that weigh the prompt's token
count (cache-aware policies, load estimates) see one token per media item where
the worker will schedule the full placeholder run. The E2E_MM_PROCESSING=worker
e2e lanes run the multimodal suites in this mode, and
crates/multimodal/scripts/check_worker_anchor_parity.py checks that a spec's
anchor is the token vLLM expands.
Both pipelines (the router's own and --mm-processor smg on the worker)
decode JPEGs through libjpeg-turbo's TurboJPEG library when libturbojpeg.so.0
loads, so their pixels are the ones an engine that decodes with Pillow sees.
Without the library they fall back to a pure-Rust decoder whose pixels differ
by a few levels on up to a fifth of a JPEG's bytes (no JPEG of the parity
corpus decodes identically), and the only marks of that state are the start-up
line libturbojpeg not found: JPEGs decode with the pure-Rust decoder ... and
the gauge smg_mm_turbojpeg_available at 0. The TurboJPEG policy makes the
state fatal instead: --mm-turbojpeg require on the router (SMG_MM_TURBOJPEG
for a process without the flag, mm_turbojpeg in a config file) stops the
router at start-up, and --mm-turbojpeg require on the worker
(SMG_VLLM_MM_TURBOJPEG, a supported spelling beside the flag, not a
deprecated one) refuses to build smg's pipeline at launch, each with an error
naming the library and the gauge. The router checks the policy once at
start-up, in every routing mode, before anything binds; a worker whose
launcher was not given the setting reads SMG_MM_TURBOJPEG as the router
does. The default, prefer, keeps the fallback; an unreadable value stops
start-up as an unreadable SMG_MM_PROCESSING does.
To move fetching and processing out of the vLLM process, run the GPU-free
sidecar next to a private Redis and point the worker at it
(pip install smg-grpc-servicer[vllm,vllm-redis]):
python -m smg_grpc_servicer.vllm.mm_sidecar --model Qwen/Qwen3-VL-8B-Instruct \
--redis-url redis://127.0.0.1:6379/0 --allowed-media-domains example.com
vllm serve Qwen/Qwen3-VL-8B-Instruct --grpc --mm-processor redis \
--mm-redis-url redis://127.0.0.1:6379/0
The sidecar and the worker must agree on model, vLLM version, dtype, video
backend, media/processor kwargs and --limit-mm-per-prompt (pass the flag to
both processes; the sidecar's limit is the one that applies, the limit is
resolved per modality before hashing so equivalent spellings match, and the key
namespace is derived from all of these): the worker advertises
mm_processor=redis only while a sidecar with a matching fingerprint keeps its
hello key alive, and rejects results that disagree. Jobs and results travel over Redis lists under
smg:mm:v1:{namespace}; results carry full tensors keyed by a per-attempt job
id and expire after 120 s. Knobs: --mm-sidecar-timeout-ms
(SMG_VLLM_MM_SIDECAR_TIMEOUT_MS, 30000), --mm-sidecar-max-queue
(SMG_VLLM_MM_SIDECAR_MAX_QUEUE, 256, fail fast when the queue is deeper),
--mm-sidecar-namespace (SMG_VLLM_MM_SIDECAR_NAMESPACE, override the derived
namespace). The sidecar resolves --redis-url and --namespace the same way,
so the two processes cannot disagree on the namespace; the timeout travels
with each job as its deadline.
On the sidecar, SMG_VLLM_MM_MAX_RESULT_BYTES (default 512 MiB, lowered to
Redis's proto-max-bulk-len when that is smaller) caps an encoded result and
SMG_VLLM_MM_MAX_VIDEO_FRAMES caps video sampling as above. A result over the
cap is answered as a 400 media_too_large instead of being pushed, and a result
Redis refuses is reported to the worker at once; a sidecar timeout is not
retried by the router, since the worker already spent the whole budget on it.
Rust request path (SMG_VLLM_SERVICER_IMPL=rust)
The same vllm.grpc.engine.VllmEngine contract can be served from Rust, with
Python keeping only the lifecycle. The switch is a flag inside this package,
not a second server, and it needs no vLLM change: vLLM's gRPC launcher imports
this package's servicer classes before it defines serve_grpc, and that
import installs the switch over serve_grpc (launcher_switch.py), so the
launcher asks the package which implementation to run before it builds an
AsyncLLM and hands the process to smg_grpc_servicer.vllm.serve_rust when
the answer is rust. That function launches the engine headless (vllm serve --headless), which dials a same-host ZMQ handshake, and serves the gRPC
contract from smg.servicer.VllmGrpcServer on a Rust-owned thread. The
Router cannot tell the two apart.
# Python (default): upstream's gRPC server, AsyncLLM in-process.
vllm serve Qwen/Qwen3-0.6B --grpc --port 50051
# Rust request path, same entrypoint. The flag is this package's: vLLM loads
# it as a general plugin while it builds the parser (smg_grpc_servicer/vllm/plugin.py);
# the stock launcher's own parser (`python -m` below) takes it too.
vllm serve Qwen/Qwen3-0.6B --grpc --port 50051 --servicer-impl rust
# The environment form, as a deployment sets it, works for every entry: `vllm serve --grpc`,
# and `python -m` on the launcher module (`vllm.entrypoints.grpc_server` is the launcher itself
# on vLLM 0.28 and 0.29, and from 0.30 a shim importing `vllm.entrypoints.launchers.grpc_server`).
SMG_VLLM_SERVICER_IMPL=rust python -m vllm.entrypoints.grpc_server --model Qwen/Qwen3-0.6B --port 50051
What the switch runs ahead of serve_grpc is the check a launcher could also
carry in its own source:
from smg_grpc_servicer.vllm import resolve_servicer_impl, serve_rust
if resolve_servicer_impl(args) == "rust":
raise SystemExit(await serve_rust(args))
Metrics (--metrics-port)
The Rust servicer serves Prometheus metrics on a listener of its own when
asked: --metrics-port <port> (--metrics-host picks the interface; it
defaults to the gRPC --host), or the environment form
SMG_VLLM_SERVICER_METRICS_PORT / SMG_VLLM_SERVICER_METRICS_HOST that the
flags override. Without either, no endpoint. The families are the same on every
engine (smg_servicer_*, labels model, rank, wire): requests by finish
reason, refusals, errors, in-flight requests, prompt / cached / generated
tokens, and the time-to-first-token, inter-token, per-request-mean and
end-to-end latency histograms (vLLM's own bucket edges), plus the servicer's
lifecycle state and start time. The listener binds beside the gRPC one, before
the engine connects (a scrape during the engine's start answers
smg_servicer_state{state="starting"} 1; a readiness hold (admission open to
the warm-up's own requests only) reads warming_up, a drain draining), runs
on its own thread, and closes
with the servicer; a port equal to --port is refused before the engine
starts, and the flag needs --servicer-impl rust (the Python servicer exposes
no endpoint). A planned warm-up's own requests go through the request path
and are counted like any other, so the first scrapes include them.
vllm serve Qwen/Qwen3-0.6B --grpc --port 50051 --servicer-impl rust --metrics-port 29100
curl -s http://127.0.0.1:29100/metrics | grep smg_servicer_requests_total
Supported vLLM releases
The switch binds serve_grpc in the launcher module of the installed release
(launcher_switch.LAUNCHERS, newest first):
| vLLM | launcher module | what python -m vllm.entrypoints.grpc_server runs |
|---|---|---|
| 0.30 and later | vllm.entrypoints.launchers.grpc_server |
a shim importing the launcher, which is bound as it loads |
| 0.28 and 0.29 | vllm.entrypoints.grpc_server |
the launcher itself, as __main__ |
Verified on 0.28 through 0.31 (launcher_switch.SUPPORTED_VLLM_RELEASES),
for every entry. vllm serve <model> --grpc imports the launcher under its
name after the parse, and the import-time hook (or the finder) binds it.
python -m on the launcher module itself runs it as __main__, where no
import hook reaches it, so it is bound in its own namespace from the two
moments this package runs inside the launcher's main() once serve_grpc
exists: vLLM loads the plugin while the launcher builds its parser
(AsyncEngineArgs.add_cli_args), and the plugin's parse step runs on the
launcher's own parse. A main-branch build between two releases carries the
next release's layout under the earlier version; the binding goes by what is
loaded, never by the version. A launcher run as a file path, or with the
plugin left out of VLLM_PLUGINS, is not reached; the Python servicer then
refuses the flag with one line.
The CPU smoke behind that table (scripts/launcher_switch_smoke.py) runs each
launcher through to the servicer construction against a stub engine, in a
fresh interpreter per case, and checks that Rust takes the process before any
engine exists and that the Python default still constructs the servicer:
# The vendored upstream launchers (tests/launchers/, one per layout); no vLLM needed.
pytest -q grpc_servicer/tests/test_vllm_launcher_switch_main.py
# A launcher tree extracted from a wheel (vllm/entrypoints/**/grpc_server.py), the rest stubbed.
python -I grpc_servicer/scripts/launcher_switch_smoke.py --launcher-tree <dir> --servicer-root grpc_servicer
# The installed vLLM (a CPU-only container with the engine image is enough): its real launcher,
# CLI, parser and plugin loading, with only the engine stubbed; no model is read.
python -I grpc_servicer/scripts/launcher_switch_smoke.py --installed
The switch verifies its hook at start-up: once the Rust implementation is
selected, the launcher's own argument parse (the plugin's parse step) checks
that the switch is bound over the launcher (the table above; a launcher run
as __main__ is checked in its own namespace), importing the module under
its known names where the CLI has not yet, and a launcher the switch cannot
bind (one without serve_grpc, a release that moved the module) is refused
with one ERROR line naming the installed vLLM version and the launchers the
switch binds, exit code 2, before the engine is built, rather than by the
Python servicer's guard after the model has loaded.
smg serve --backend vllm --connection-mode grpc --servicer-impl rust sets
the flag in each worker's environment, after checking that the installed
vLLM's launcher will consult it (smg_grpc_servicer.vllm.rust.upstream_hook_installed:
a launcher at a module path the switch binds imports this package at module
level, or a launcher anywhere carries the check itself); the
Python servicer refuses to start when the flag asks for Rust, so a launcher
that reaches it anyway fails loudly instead of silently running Python. The
headless engine is launched from the parsed namespace through vLLM's own
run_headless, so both entrypoints above work unchanged.
Rust mode needs the smg wheel (for the binding) and serves the whole
contract the Python servicer serves: text generation, PD disaggregation
(--kv-transfer-config: connector params pass through both ways and
GetServerInfo carries the pairing identity), Router-preprocessed media
(inline and /dev/shm tensors), worker-side media processing (media_refs,
below), Embed, FlushCache, StartProfile and StopProfile (the engine's own
torch profiler as its launch configured it, started and stopped on every rank;
a start that sets per-request options is refused, since the engine takes its
profiler's directory and options at launch; the same on the Python servicer),
GetTokenizer (which answers
FAILED_PRECONDITION when the launcher could not resolve a local tokenizer
directory) and SubscribeKvEvents (vLLM's ZMQ publisher, which the servicer
turns on by itself, on this path as on the Python one, when the launcher is
given no --kv-events-config, at a port found free on the host rather than
vLLM's fixed default, so two engines on one host do not collide; an explicit
one is kept as given, its endpoint included, and
SMG_VLLM_SERVICER_KV_EVENTS=0 leaves the publisher off). Tuning:
SMG_VLLM_SERVICER_HANDSHAKE_PORT (default: a free port),
SMG_VLLM_SERVICER_DRAIN_SECS (default 5),
SMG_VLLM_SERVICER_STARTUP_TIMEOUT_SECS (default 1800: how long the servicer
waits for a sign of life from the engine during its handshake; the launcher
reports the engine process alive each time it polls it, and the handshake's
own messages count too, so a healthy start that takes longer, a large
checkpoint streaming in for an hour, still completes, while an engine that
exits fails fast regardless), SMG_VLLM_SERVICER_STARTUP_CEILING_SECS
(default 14400: the most a start may take however alive the engine is; 0
lifts the ceiling), SMG_VLLM_SERVICER_SILENCE_TIMEOUT_SECS (default 300: how
long the connected engine may hold requests in flight, in total since its last
output, with nothing pending prefill, before the servicer treats it as dead,
fails them and closes the link for the launcher to restart the pair; a hard
engine death sends nothing over the wire, so silence is the only sign of it),
SMG_VLLM_SERVICER_PREFILL_TOKENS_PER_SEC (default 2000: the prompt tokens
pending prefill, those of the requests the engine has accepted and produced
nothing for yet, extend that bound by the time they take at this rate, 500 s
per million tokens; a wire that says nothing until a prompt's first token is
silent through a whole prefill, and a batch of long prompts prefills for
longer than any fixed bound; a low rate is a bound on how slowly a healthy
engine may prefill, not an estimate of how fast it does; 0 allows nothing),
SMG_ZMQ_SOCKET_DIR, SMG_SERVICER_WORKER_THREADS
(default 1, for the one runtime server.rs builds for the vLLM, SGLang and
TokenSpeed Rust servicers alike: one runtime worker carries about 100k output
tokens/s on one connection with its lock uncontended; raise it to 2-4 when one
engine serves more than about 512 concurrent streams and their inter-token p99
matters more than the servicer's CPU).
Prefill/decode over NIXL on one host: restarting a leg in place
A prefill and a decode leg on one host (or one NVLink domain) move KV over UCX's CUDA IPC: the importing side opens the peer's allocations and keeps the mappings in a process-wide cache that lives as long as the importer does, and an imported mapping keeps the exporter's allocation alive after the exporter died. A leg that crashes therefore cannot restart on its own GPU until its peers let go: its startup memory check sees the dead process's memory as still in use ("Free memory on device ... is less than desired GPU memory utilization") and the pod crash-loops until the peers restart too. Two of the engine's own settings bound the hold, and they work only together:
UCX_CUDA_IPC_CACHE=nin the engine's environment: a mapping ends with its transfer instead of staying cached.kv_connector_extra_config.engine_ttl(seconds) in--kv-transfer-config: a peer idle longer than this is dropped the next time the connector runs its sweep, which it does when it handshakes a new peer and when it sends a heartbeat; an hour by the connector's default. A restarted leg counts as a new peer only for the peers that do not hold its memory: the leg whose memory a peer still holds fails its startup memory check before it handshakes anyone, so its own restart cannot run the sweep that would release it. The heartbeat goes out on a known peer's requests as well, at most once per sixth of the connector's lease (five seconds at its default) while any request is waiting for its KV, so a busy decode runs the sweep every few seconds; the peer a request reads from is stamped active before the sweep, so a pair's own traffic never drops the pair. A single pair whose prefill died sends no heartbeat either; its decode lets go of the dead leg's memory only when another peer turns up or a live peer's requests run the sweep. Dropping the peer releases its transfer state, the transfers that were in flight when it died included; with the cache off their mappings go with them, with the cache on the mappings outlive the peer either way.
Neither is free, which is why the servicer does not set them for you. With the cache off, every request's transfer opens and closes each of the peer's allocations it touches (about one per layer, the way the engine allocates its KV cache). Measured on one host, two tensor-parallel-2 legs over CUDA IPC, prompts of about 5100 tokens, 40 sequential requests per arm: the first token came 2 % later at the median (885 to 903 ms) and 5 % later at the 90th percentile (900 to 943 ms), with one three-second outlier in forty, in the cache-off arm. A short TTL costs a quiet pair nothing by itself, since the sweep runs only when a new peer turns up or a heartbeat goes out on a request; when either happens, a healthy pair that had been idle longer than the TTL (other than the pair whose own request ran the sweep) is dropped with the dead ones and runs its handshake again on its next request (the peer's metadata plus a descriptor list over every one of its KV blocks: seconds on a large cache). Legs on different hosts are not affected: over TCP or RDMA nothing of the peer's memory is imported, and a restart passes the memory check at once. The repository's end-to-end harness runs its NIXL pairs with the cache off and a TTL of 3 s because it restarts legs on purpose; a production pair on one host wants a TTL of a few minutes, longer than any gap a busy pair leaves, shorter than the restart back-off a crashed leg would otherwise sit through.
Planned warm-up before SERVING (SMG_VLLM_SERVICER_WARMUP)
The engine selects and compiles kernels lazily: the first scheduler step at a
token count it has not seen (a new prefill length class, a new running-batch
size, a count off the alignment its compiler specialised on) pays a kernel
search or a compile of up to several seconds, and every request in flight
during that step waits. The engine's own start warms a fixed set of shapes
(its graph-capture sizes, one tuning pass at the admission step's full token
count), so the shapes a traffic ramp reaches first were paid for by the ramp's
requests, on every engine of a fleet at once. Both servicers now run a planned
warm-up over the engine before they report SERVING: a decode staircase (as
many concurrent requests as the running window holds, each asking for one more
token than the last, so every running-batch size runs once), a prefill ladder
(one request per 128-token length class up to --max-num-batched-tokens, the
graph-capture sizes, lengths one above the tuners' bucket grid), a second
pass over the bucket grid whose timings show what the first pass paid, and,
where the engine's module asks for it (TokenSpeed, below), every remaining
prompt length up to the step, last, in an order whose every prefix is a
uniform spread of the range, so a budget cut leaves an even coverage. The
longest prompt is what the engine's scheduler admits with one output token:
the context less the positions the engine keeps back (none on vLLM, where a
prompt and an output budget may fill the context; two on TokenSpeed, or its
speculative draft window when wider; six on SGLang), never the context
itself. Every request carries its own token ids, so prefix caching never
hides a prefill, and the engine's prefix cache is flushed at the end. One INFO line per shape
with its time (a chunk remainder's at DEBUG, with an INFO progress line every
500 remainders and one at their end); readiness (the gRPC health service, so the startup and
readiness probes) stays NOT_SERVING until the pass ran, and flips SERVING
whatever the pass returned (a failed request is a logged line, never a worker
kept out of rotation). SMG_VLLM_SERVICER_WARMUP = full (the default:
every 128-token class; about a minute on a 16k step), fast (the tuners'
bucket grid only: seconds), off, a budget in seconds (120) or
<level>:<seconds> (fast:30); the budget (default 300 s; 900 s for
TokenSpeed, whose plan holds the chunk remainders, below) bounds the pass:
it ends between shapes, the remaining ones named in the log, and each
request's timeout is clamped to what is left of it (one second at least), so
a shape that hangs cannot hold readiness past the budget. On the Rust path the
pass runs over the servicer's own gRPC port between the engine's handshake and
SERVING, once the KV-event relay's subscription has reached the engine's
publisher (the warm-up's batches are the engine's first, and a subscription
that landed after them would leave the relay's history starting late for the
engine's life; the relay dials the publisher every second from the servicer's
start, so the subscription is normally in place before the handshake, and the
wait is bounded at 30 s with a logged fall-through); the Python servicer runs
it over its in-process engine with its health service holding NOT_SERVING
meanwhile. Under data parallelism every
rank is a scheduler of its own: the plan runs the staircase once per rank and
every prefill shape on every rank at once, each request pinned to its rank
(data_parallel_rank), so the log's step count is per rank.
Worker-side media processing uses the same --mm-processor /
SMG_VLLM_MM_PROCESSOR setting as the Python servicer, with one more choice:
inprocess(vLLM's MediaConnector and the engine's renderer) andredis(the sidecar) are the Python servicer's backends, so the media is fetched and processed by vLLM's own code on either servicer. The Rust server hands a request'smedia_refsto the Python bridge (smg_grpc_servicer.vllm.rust_media), which runs the processor and vLLM's input processor on the launcher's asyncio loop and returns the expanded prompt andmm_featuresas vLLM's own encoder writes them; Rust relays the encoded features to the engine untouched and sends the tensor frames straight from the memory Python lent it, without a copy.smgis smg's own media pipeline, the one the Router runs for--mm-processing router, run inside the Rust servicer with no Python on the request path: fetch, decode, preprocess, placeholder expansion, and the batches a Router-preprocessed request would carry, translated for the engine the same way. It serves the model families that pipeline supports (crates/multimodal), takeshttp,httpsanddatareferences, reads the model'sconfig.jsonand preprocessor configs from the tokenizer directory the launcher resolved, lays the engine's--mm-processor-kwargsover those preprocessor configs as the engine's own processor would take them (lessdevice, which only says where that processor would run; a key the pipeline has no field for is refused at launch), refuses a request above the engine's own--limit-mm-per-promptas the engine's server does (--mm-max-itemsandSMG_*_MAX_COUNTtighten those limits, never loosen them), refuses a media item whose features exceed the engine's encoder budget (mm_encoder_budgetabove) before it reaches the engine, as the engine's own front end does, with the item's feature tokens and the budget in the same message, and samples a video to the engine's own frame budget (--media-io-kwargsvideo.num_frames, else its loader's default) where the family's processor takes the loader's frames, so a clip costs the same tokens as on the engine's own server; the budget is read under the sameSMG_VLLM_MM_MAX_VIDEO_FRAMEScap the in-process and Redis processors apply, so an unbounded count stays bounded insmgmode too (a cap below the engine's count then samples fewer frames than the engine's own server would, as intended), and a loader rule the pipeline cannot follow (video_backendin the engine's media kwargs, elseVLLM_VIDEO_LOADER_BACKEND, other thanopencv, or avideo.fpsabove zero, which thins frames by duration) is refused at launch for a family that samples the way the loader does (Gemma 4) and serves video at all; the families with a rate-based sampler of their own never followed the loader and start as before. An engine that normalizes pixels on device (vLLM'smm_device_do_normalize, on by default for the Qwen-VL family) takes rawuint8pixels, and the pipeline writes those for it; a model whose processor cannot emit raw pixels is refused at startup under that setting (start the engine with--mm-device-do-normalize=falseor useinprocess). The Python servicer refusessmg: it has no such processor, and the launcher's switch refuses it before the engine is built and the model loaded (one configuration-error line, exit code 2), not after.
An audio reference (a family whose audio the engine's own processor takes)
reaches inprocess and redis on the Rust servicer as it does on the Python
servicer; the smg pipeline takes image and video references and refuses an
audio one before any work, naming the modes that take it.
The in-flight cap, the saturation refusal, the advertised mm_processor /
mm_media_ref_schemes / mm_processor_source and the PD prefill side's
media_identity behave as on the Python servicer whichever processor runs;
the engine-side contract does not change.
Known difference: under --structured-outputs-config.backend auto (the
default) vLLM's frontend validates each constraint with xgrammar and falls
back to guidance when xgrammar rejects it. Rust mode applies the same static
rules (JSON-schema features xgrammar lacks go to guidance, a choice becomes
the grammar xgrammar compiles), but it cannot run xgrammar's parser, so a regex
or grammar that only guidance accepts fails that request at the engine's
grammar compile instead of falling back. Pin guidance (or xgrammar)
explicitly when that matters; the engine keeps the backend of its first
structured request either way, as it does behind vLLM's own frontend.
A constraint the engine's backend cannot enforce is refused before the engine
with INVALID_ARGUMENT (a 400 at a router), the same way on every Rust servicer:
a JSON-schema feature xgrammar drops (multipleOf, uniqueItems, a string
format it lacks, ...) wherever xgrammar is the backend, whether the schema is
a response_format's or inside a forced tool call's structural tag; every kind
of constraint on an engine with no grammar backend; a kind the backend does not
compile. The vLLM servicer reads the backend off --structured-outputs-config,
the SGLang and TokenSpeed servicers off their engine's --grammar-backend (see
their sections); a backend the servicer does not know is left to the engine.
A constraint the engine admitted and then could not continue the request under
(a sampled token the grammar rejects, as speculative decoding can produce) is
one status on every servicer that can tell it apart, Rust and Python:
FAILED_PRECONDITION with the trailer smg-constraint-failed: engine, the
message naming the constraint and keeping the engine's own words. Behind the
servicers a router answers the client a 422 under the code constraint_failed,
replays nothing (the same tokens fail the same way, each replay a prefill), and
samples no circuit breaker against the worker; a router that predates the
trailer answers a 400 for the code and retries nothing. The status is raised
only on the engine's own attribution, never inferred from the request carrying
a constraint: an engine aborts or fails requests for other reasons too (a pause
or an abort-all, a decode side's failed KV handoff, a capacity abort), and
those keep the status they have on any request. Per engine:
- SGLang: the scheduler's grammar paths end such a request with a bare
FINISH_ABORT()(messageAborted, no status, noerr_type), the finish an abort-all gives every running request too. This package's SGLang plugin (smg_grpc_servicer.sglang.zmq_plugin, loaded in every scheduler rank) hooks those paths (SchedulerBatchResultProcessor._accept_grammar_tokens,_apply_prefill_grammar, and the PD decode side'sprocess_prebuilt) and stamps the finish they set witherr_typeGrammarRejectedToken; the ZMQ wire carries it in the slim batch'sfinished_err_typescolumn, the Python servicer reads it off the finish dict. The hooks register under the ZMQ handshake and underSMG_SGLANG_ATTRIBUTE_GRAMMAR_ABORTS=1, which the Python servicer sets for the ranks it spawns; a release without one of the paths keeps that path's aborts bare, which the servicers answer as before. - vLLM: the engine finishes a grammar rejection, a grammar compile error, a
failed KV load, an encoder-cache miss and a pre-processing fault alike with
error, and a pause or shutdown withabort; nothing on its wire names the cause, so anerrorstays the engine's own report (retried, charged and probed as such) and anabortkeeps its shape. - TokenSpeed: the engine's abort codes name transfer, numerical and capacity failures and the pause's abort, none the grammar (a rejected token is dropped on the speculative path); the slim wire carries the finish type alone. Its finishes keep their shapes.
Both vLLM servicers also advertise the engine's reasoning parser
(engine_reasoning_parser; reasoning_parser is the per-model parser override
a router reads off a worker's labels) and where its grammar starts on a prompt
that ends inside the model's thinking block (constrained_decoding_start): after_reasoning when
a reasoning parser is set and enable_in_reasoning is off, since vLLM then
holds a request's grammar back until the parser has seen the thought's end,
first_token otherwise. A router shapes a forced tool call's grammar by it as
the engine's own server would: the bare calls behind the parser's gate, so the
call is forced right there; the thought and then the calls from the first
token.
CUDA-graph capture cap
The engine replays a captured CUDA graph for a step that fits one of its
capture sizes and runs a larger step eagerly, kernel by kernel from Python.
Its default for the largest size (max_cudagraph_capture_size) is twice
--max-num-seqs, at most 512 (1024 on one newer GPU platform): with a window
of 32 that is 64 tokens, so a step that admits a prefill chunk beside the
decodes runs eagerly, and an engine whose eager step is expensive (a
sparse-attention or MoE model: hundreds of launches per rank) is host-bound at
every admission. A gRPC launch (vllm serve --grpc, the stock launcher, and
vllm serve --headless for the engine-only ranks of a multi-node engine)
therefore derives the cap from the admission step when the operator set none:
--max-num-batched-tokens (unset: unbounded, the engine's own step-size
defaults are 2048 or more), at most 512, applied only where it raises the
engine's default, for both servicer implementations, and logged once at start
(CUDA-graph capture cap: ...). The engine's rule is kept: the value is the
step size itself or a value of the engine's capture grid (1, 2, 4, multiples
of 8 to 248, multiples of 16 from 256), which the engine neither clamps nor
truncates.
| setting | engine default | launch default |
|---|---|---|
--max-num-seqs 32 --max-num-batched-tokens 16384 |
64 | 512 |
--max-num-seqs 32 --max-num-batched-tokens 300 |
64 | 300 |
--max-num-seqs 32, step size unset |
64 | 512 |
--max-num-seqs 32 --max-num-batched-tokens 48 |
48 | the engine's |
--max-num-seqs 256 |
512 | the engine's |
--max-num-seqs above 256 |
512, or twice the window up to 1024 on one platform | the engine's |
--max-num-seqs unset |
512 or 1024 by platform (the engine's window defaults) | the engine's |
--max-num-seqs unset, --max-num-batched-tokens 16384, SMG_VLLM_CUDAGRAPH_CAPTURE_CAP=2048 |
512 or 1024 by platform | 2048 |
Kept as given: an explicit --max-cudagraph-capture-size,
--cudagraph-capture-sizes or the same keys of --compilation-config;
--enforce-eager and a cudagraph_mode of NONE (no graphs); a
--speculative-config (the engine's default adds the uniform-decode sizes an
explicit cap drops); an HTTP vllm serve, which is not this package's launch.
SMG_VLLM_CUDAGRAPH_CAPTURE_CAP is the knob: auto (the default), engine
(the opt-out: the engine's own default, as before this derivation), or a
positive integer, the ceiling in place of 512, rounded down to the capture
grid. A ceiling applies only where it does not lower the engine's default on
any platform (twice the window, at most 512, or at most 1024 on one newer GPU
platform the launch cannot tell apart, each as the engine's list ends it):
with --max-num-seqs left to the engine or set to 512 or more, a ceiling of
1024 or more applies (it raises the 512 default and, on the platform whose
default is already 1024, matches it); for a window between 257 and 511, a
ceiling of twice the window (on the capture grid) or more applies; a smaller
one is kept out, the line saying why. Each captured size costs capture time at start and
graph memory per rank: 11 sizes at the engine's 64, 51 at 512, 83 at 1024,
147 at 2048.
Per recipe: what a ceiling above 512 buys, and what it costs
The ceiling of 512 is the engine's own default ceiling, so the launch default
never spends more on graphs than the engine does for itself at a large window,
and it is the right cap where the admitted step stays under it. Under
open-loop arrivals with chunked prefill and prefix caching, the step that
decides capacity holds up to --max-num-seqs decode tokens plus the prefill
chunks admitted beside them, so on a large running window that step is above
512 and runs eagerly at every arrival. Measured on three recipes (the same
build and recipe on both arms, one engine per arm, open-loop Poisson ramps of
150 s per step; SLO-1 = TTFT p99 under 1 s at goodput >= 0.9, SLO-2 = TTFT
p99 under 400 ms and TPOT p99 under 25 ms at goodput >= 0.95; capacity = the
highest offered rate at which the SLO holds, in requests per second per
engine; the costs are per engine against the 512 arm: KV-cache tokens,
container memory at the ramp's end, start-up time):
| recipe | 512 (the launch default) | 1024 | 2048 |
|---|---|---|---|
--max-num-seqs 32 --max-num-batched-tokens 16384, a 1M-context model (the engine's own 64); 8k prompts, 4k shared prefix |
SLO-1 12, SLO-2 11 | SLO-1 12, SLO-2 8; -0.5 % KV, +2.0 GiB, +45 s | SLO-1 12, SLO-2 10; -1.5 % KV, +6.1 GiB, +93 s |
--max-num-seqs 128 --max-num-batched-tokens 16384, a sparse-attention MoE (the engine's own 256); 4k prompts, 1k shared prefix |
SLO-1 28-34, SLO-2 20-22; collapses from 36 | SLO-1 40 (the ramp's top), SLO-2 24; -0.3 % KV, +1.7 GiB, +38 s | SLO-1 38, SLO-2 28; -0.9 % KV, +4.9 GiB, +126 s |
--max-num-seqs 256 --max-num-batched-tokens 16384, a 3B-active MoE (the engine's own 512: the derivation changes nothing); 8k prompts, 4k shared prefix |
SLO-1 11-12, SLO-2 5-6 | SLO-1 13-14, SLO-2 7-8; -0.15 % KV, +1.5 GiB, +27 s | SLO-1 20 (the ramp's top), SLO-2 14; -0.5 % KV, +4.7 GiB, +60 s |
The rows are same-build pairs on the engine release before the shipped one. On the shipped release the 128-sequence recipe was run at 1024 only: SLO-1 24 req/s (22.9 achieved, TTFT p99 394 ms, TPOT p99 38 ms), no SLO-2, a collapse from 28 with the running window full and the decode step near 75 ms at 127 running. That release's step at a full window bounds the bed below the previous release's 512 knee, and without a 512 arm on it the ceiling's own effect on the shipped release is not separated from the release's step time (that arm is owed); the rows above are the measured basis of the rule, not a reading of the shipped release.
The cap that captures a recipe's gain follows its running window (about eight
times --max-num-seqs on these three), not one ceiling: with a window of 128,
1024 buys the whole of 2048's gain at a third of the memory; with 256 the gain
needs 2048; with 32 nothing above the launch default buys anything (on the
128-window recipe, 8k prompts at a 94 % cache hit held the ramp's top on 512
and 2048 alike: that shape's knee lay above the ramp, which is why the wide
shape is the one in the table). The launch default therefore stays at the
memory-neutral 512, and a recipe whose class gains sets its own cap: the
engine's own --max-cudagraph-capture-size N among the recipe's flags (it
travels with the command line to every rank of a multi-node engine), or
SMG_VLLM_CUDAGRAPH_CAPTURE_CAP=N in the environment of every rank's process,
a ceiling the derivation still bounds by the admission step and applies where
it raises the engine's default on every platform. Graph memory per captured size was
0.011-0.021 GiB per rank on these three models, which is how the KV-cache
cost above scales with the capture list.
Multi-node engines: the worker pod's liveness
An engine whose tensor-parallel ranks span pods runs its EngineCore and the
gRPC servicer on a leader pod (--nnodes N --node-rank 0 --master-addr <its own address> --master-port <port>) and the other ranks on worker pods
(vllm serve <model> --headless --nnodes N --node-rank <k> --master-addr <the leader>, the same engine flags). Every rank joins the process group
through the leader's TCP store at --master-port and keeps that connection
for its lifetime. When the leader's engine goes away (the pod restarts, or
the servicer gives up on a start), the ranks on the worker pods are not
told: their store connection is dead and they wait on it, the next leader
waits for ranks that never join, and a worker probe that only asks whether
the engine process exists (grep vllm /proc/1/cmdline) keeps those orphans
running until someone deletes the pod. A leader restart must restart its
workers: ranks cannot re-join a store they were not started against.
python -m smg_grpc_servicer.vllm.worker_probe is the worker pod's probe.
It passes while at least one TCP connection from the pod to the leader's
store port is established and fails otherwise, so the kubelet restarts the
worker and its fresh ranks join the new leader within periodSeconds x failureThreshold. Without arguments it reads --master-addr and
--master-port off the worker's own command line (/proc/1/cmdline);
--leader <name> names the leader by its stable DNS name instead,
re-resolved on every run, so that a recreated leader pod at a new address
fails the probe even while the stale connections to the old one linger, and
--port names the store port. The ranks connect to the store early in
their start, before the model loads, so the same command serves as the
startup probe, with a budget that covers waiting for the leader. Run it
with python3 -I: the probe itself needs only the standard library, but
the engine's PYTHONPATH may carry site customizations that import the
engine, and an interpreter start on a node saturated by the weight stream
takes seconds; -I leaves both out, and timeoutSeconds of 30 or more
keeps a slow start from counting as a failure:
# the worker pod's container; LEADER_HOST is the leader's stable DNS name
startupProbe:
exec: {command: [/bin/sh, -c, 'python3 -I -m smg_grpc_servicer.vllm.worker_probe --leader "$LEADER_HOST"']}
periodSeconds: 30
timeoutSeconds: 30
failureThreshold: 480
livenessProbe:
exec: {command: [/bin/sh, -c, 'python3 -I -m smg_grpc_servicer.vllm.worker_probe --leader "$LEADER_HOST"']}
periodSeconds: 30
timeoutSeconds: 30
failureThreshold: 3
MLX
python -m smg_grpc_servicer.mlx --model meta-llama/Llama-2-7b-hf --host 0.0.0.0 --port 50051
TokenSpeed
# Python (default)
python -m smg_grpc_servicer.tokenspeed --model meta-llama/Llama-2-7b-hf --host 0.0.0.0 --port 50051
# Rust request path, same entrypoint
python -m smg_grpc_servicer.tokenspeed --model meta-llama/Llama-2-7b-hf --host 0.0.0.0 --port 50051 --servicer-impl rust
# The environment form, overridden by the flag when both are given
SMG_TOKENSPEED_SERVICER_IMPL=rust python -m smg_grpc_servicer.tokenspeed --model meta-llama/Llama-2-7b-hf --port 50051
Without --kv-events-config, the servicer turns TokenSpeed's ZMQ KV-event
publisher on by itself, under either implementation, before the engine is
built, at a port found free on the host rather than TokenSpeed's fixed default,
so SubscribeKvEvents relays the events a cache-aware router routes by and
workers that share a host do not collide; an explicit configuration is kept as
given, its endpoint and off included, and SMG_TOKENSPEED_SERVICER_KV_EVENTS=0
leaves the publisher off. An encode worker (--disaggregation-mode encode) has
no KV cache and is left as given. A hybrid model (Gated DeltaNet, Mamba or KDA
layers) keeps its recurrent state in State groups of TokenSpeed's cache, which
span no tokens, and TokenSpeed's KV-event drain aborts the engine on such a
group: for a model whose layer labels the engine's own cache recipes map to a
State group, the servicer leaves the publisher off and logs why once
(KV event publishing left off: ...); --kv-events-config turns it on anyway.
This is the process ts serve spawns for its gRPC worker; --servicer-impl
is this package's flag (listed first by --help, ahead of TokenSpeed's own
flags), and SMG_TOKENSPEED_SERVICER_IMPL is the fallback the flag overrides
(flag, then the environment, then python; the launcher logs which one
decided). With --servicer-impl rust the same process serves the
tokenspeed.grpc.scheduler.TokenSpeedScheduler contract from Rust
(smg.servicer.TokenSpeedGrpcServer, which needs the smg wheel): the
launcher computes the model and server facts from TokenSpeed's own config,
runs the scheduler(s) headless in a spawned child over the msgpack ZMQ wire,
and supervises both. What that wire does not carry is reported, not emulated:
FlushCache and profiling ride the wire's control call on a scheduler whose
handshake declares its control reply (the control-wire revision) and answer
UNIMPLEMENTED, naming the gap, on one that does not; ranked top_logprobs and
prompt logprobs are refused, and the RL control-plane extras (the rl.*
advertisement, the live weight_version on generate responses, is_paused)
stay with the Python implementation; the Rust server redacts credentials from
server_args the same way. Prefill/decode disaggregation is served on this
path: a request's kv_bootstrap_info rides the wire in the scheduler's own
bootstrap slots as the Python servicer forwards it, a prefill launch starts
the engine's KV bootstrap server in the launcher process before the ranks (as
the engine's own frontend does), and under attention data parallelism the
servicer places a disaggregated request on the rank of its room's residue,
the engine's own dispatch rule, and an aggregated one on its
data_parallel_rank pin. The EPD prefill side is served the same way (each
item's encode_bootstrap_info entry rides on the item); the EPD encode role
(--disaggregation-mode encode) is refused at launch under --servicer-impl rust and stays with the Python implementation, whose encode servicer feeds
the encode worker over the engine's in-process channel. SubscribeKvEvents relays
TokenSpeed's ZMQ KV-event publisher, which the servicer turns on by itself, on
this path as on the Python one, when the launcher is given no
--kv-events-config, at a port found free on the host rather than
TokenSpeed's fixed default (an explicit one is kept as given, its endpoint
included; SMG_TOKENSPEED_SERVICER_KV_EVENTS=0 leaves the publisher off; a
hybrid model's State groups leave it off, as above).
Tuning:
SMG_TOKENSPEED_SERVICER_HANDSHAKE_PORT (default: a
free port), SMG_TOKENSPEED_SERVICER_DRAIN_SECS (default 5),
SMG_TOKENSPEED_SERVICER_WARMUP (the planned warm-up before SERVING, as for
vLLM above: full by default, fast, off, a budget in seconds or
<level>:<seconds>; planned from --max-num-seqs and the chunk size or
--max-prefill-tokens; at the full level, on an engine of more than one
process (its parallel layout off the server arguments; a one-process engine
plans the classes alone, like the other engines, since the hang below needs
a collective waiting on a peer), this engine's plan also visits
every prompt length up to the chunk size the class ladder leaves out, last,
in an order whose every prefix is a uniform spread (fast plans the classes
alone and leaves the hazard below open, which the start log says): a chunked prefill's last chunk is any count
below the chunk size, a prefill chunk reaches two cuBLAS GEMMs here (the
compressor's fp32 router projection above 32 tokens, the grouped output
projection's batched GEMM) whose kernels the library picks per token count
and loads on first use, and such a first use during serving, with the
overlapped previous chunk's TP collective in flight, has deadlocked the
forward; at a 4,096-token chunk that is 4,042 one-token requests after the
ladder's 54, about 0.17 s each on a four-GPU node (692 s measured for the
whole pass, inside this engine's 900 s default budget; with no chunk size set
the engine's default step of 8,192 tokens gives about 8,140 remainders, about
1,380 s, which the budget cuts about halfway, evenly: set a chunk size or a
larger budget to cover every count); the Python servicer runs the same plan
over its own port after its first warm-up request, and both skip it on a
disaggregation role; under attention data parallelism the servicer routes a
request by load, not by rank, so each prefill shape, the chunk remainders
included, warms one rank and the log says so: there the remainder hazard
stays open on the ranks a count did not reach until the wire can pin a rank),
SMG_TOKENSPEED_SERVICER_STARTUP_TIMEOUT_SECS (default 1800) and
SMG_TOKENSPEED_SERVICER_STARTUP_CEILING_SECS (default 14400; the two startup
bounds as for vLLM above: the launcher reports the scheduler process alive each
time it polls it, so a slow but living start completes, under the ceiling, and
0 lifts the ceiling), SMG_TOKENSPEED_SERVICER_SILENCE_TIMEOUT_SECS (default
300) and SMG_TOKENSPEED_SERVICER_PREFILL_TOKENS_PER_SEC (default 2000; the
connected scheduler's silence bound and its prefill allowance as for vLLM
above: this wire carries token batches only, so a batch of long prompts is
silent for as long as its prefill takes, and the allowance, 500 s per million
pending prompt tokens at the default rate, is what keeps that from reading as
a dead scheduler), SMG_ZMQ_SOCKET_DIR, SMG_SERVICER_WORKER_THREADS
(default 1, for the one runtime server.rs builds for the vLLM, SGLang and
TokenSpeed Rust servicers alike: one runtime worker carries about 100k output
tokens/s on one connection with its lock uncontended; raise it to 2-4 when one
engine serves more than about 512 concurrent streams and their inter-token p99
matters more than the servicer's CPU).
Both TokenSpeed servicers advertise, as server_args keys, the engine's
reasoning parser (engine_reasoning_parser; reasoning_parser is the
per-model parser override a router reads off a worker's labels) and where its
grammar starts on a prompt that ends inside the model's thinking block
(constrained_decoding_start): always first_token, since this engine's grammar
manager has no reasoning gate and starts every grammar at the first generated
token (--reasoning-parser only has a JSON-schema constraint rewritten into a
structural tag that lets the thought precede the JSON). A router shapes a
forced tool call's grammar by it as the engine's own server would: the thought
and then the calls, so the call is not forced into the thought.
They also carry the grammar backend the engine runs as the grammar_backend
key, resolved: --grammar-backend as given (xgrammar or none), else the
checkpoint's default once the engine has read it (none for most checkpoints;
the key is absent until it has). The Rust servicer refuses a constraint that
backend cannot enforce before the scheduler, as the vLLM servicer does: every
kind under none (the scheduler itself would admit the request and abort it
with a 400 that its slim wire reports as a bare abort, an empty answer to the
caller), and a JSON-schema feature xgrammar drops under xgrammar; with no key
it judges nothing ahead of the engine.
Both TokenSpeed servicers also report the engine's constrained-decoding policy,
when it declares one, as GetServerInfo.constrained_decoding (a field of its
own, not a server_args key): always for a model that takes a grammar on
every turn, requested for one only when the request asks. The servicers read
it as constrained_decoding off TokenSpeed's ServerArgs, else its
ModelConfig. A router turns the field into the worker's constrained_decoding
label, and its gRPC request building follows the selected worker's policy (under
PD the decode worker's). requested, the default, constrains what a request asks
for. always, on an engine that starts the grammar at the first token, gives
every turn the grammar of the model's response template table where that table
spells whole turns; otherwise it constrains the calls a tool request allows under
auto.
Metrics (--metrics-port)
With --servicer-impl rust, --metrics-port <port> (and --metrics-host,
defaulting to the gRPC --host) serves the Rust servicer's Prometheus metrics
on a listener of its own; SMG_TOKENSPEED_SERVICER_METRICS_PORT /
SMG_TOKENSPEED_SERVICER_METRICS_HOST are the environment forms the flags
override, and without either there is no endpoint. The families and labels are
the ones every engine's Rust servicer exposes (see the vLLM section above):
requests by finish reason, refusals, errors, in-flight requests, prompt /
cached / generated tokens, the latency histograms, the lifecycle state. A port
equal to --port is refused before the engine starts, and the flag is refused
under the Python servicer, which exposes no endpoint.
python -m smg_grpc_servicer.tokenspeed --model <model> --port 50051 --servicer-impl rust --metrics-port 29100
Multi-node engines: the worker pod's liveness
An engine whose tensor-parallel ranks span pods is launched on every pod with
the same flags, --nnodes N --dist-init-addr <the leader>:<port> among them,
and the pod's own --node-rank <k>: the launcher above on the leader pod
(--node-rank 0, where the gRPC servicer runs), the scheduler ranks alone on
the worker pods. Every rank joins the process group through the TCP store at
--dist-init-addr, which rank 0 on the leader pod hosts, and keeps that
connection for its lifetime, so the contract of the vLLM section above holds
here too: a leader restart must restart its workers, and
python -m smg_grpc_servicer.tokenspeed.worker_probe is the worker pod's
probe. It passes while at least one TCP connection from the pod to the
leader's store port is established and fails otherwise; without arguments it
reads the leader and the port off --dist-init-addr on the worker's own
command line (/proc/1/cmdline), --leader <name> names the leader by its
stable DNS name instead, re-resolved on every run, and --port names the
store port (both are needed when TokenSpeed derives the flag from a multi-node
Slurm step's environment rather than the command line). Run it with
python3 -I and a timeoutSeconds of 30 or more, as above:
# the worker pod's container; LEADER_HOST is the leader's stable DNS name
startupProbe:
exec: {command: [/bin/sh, -c, 'python3 -I -m smg_grpc_servicer.tokenspeed.worker_probe --leader "$LEADER_HOST"']}
periodSeconds: 30
timeoutSeconds: 30
failureThreshold: 480
livenessProbe:
exec: {command: [/bin/sh, -c, 'python3 -I -m smg_grpc_servicer.tokenspeed.worker_probe --leader "$LEADER_HOST"']}
periodSeconds: 30
timeoutSeconds: 30
failureThreshold: 3
SGLang
sglang serve --model-path meta-llama/Llama-2-7b-hf --grpc-mode
Without --kv-events-config, the servicer turns SGLang's ZMQ KV-event
publisher on by itself, under either implementation, before the schedulers are
started, at a port found free on the host rather than SGLang's fixed default,
so SubscribeKvEvents relays the events a cache-aware router routes by and
workers that share a host do not collide; an explicit configuration is kept as
given, its endpoint and off included, and SMG_SGLANG_SERVICER_KV_EVENTS=0
leaves the publisher off.
Both SGLang servicers advertise the engine's --limit-mm-data-per-request as
the flat mm_item_limits label (image=1,video=1) in GetServerInfo's
server_args, so a router that preprocesses media itself holds requests to
the engine's limits: the precomputed inputs it sends bypass the tokenizer
manager's own check. Without the flag the engine has no such limit and the
label is absent.
They also advertise, as server_args keys, the engine's reasoning parser
(engine_reasoning_parser; reasoning_parser is the per-model parser override
a router reads off a worker's labels) and where its grammar starts on a prompt
that ends inside the model's thinking block (constrained_decoding_start):
after_reasoning with --reasoning-parser, since the scheduler then wraps its
grammar backend in a reasoner that holds a request's grammar back until the
parser's thought-end sequence has been generated (for a request that asks for
reasoning, which a router does on a thinking prompt), first_token without
one. A router shapes a forced tool call's grammar by it as the engine's own
server would: the bare calls behind the parser's gate, so the call is forced
right there; the thought and then the calls from the first token.
They also carry the grammar backend the scheduler runs as the grammar_backend
key, resolved: --grammar-backend as given (xgrammar, outlines,
llguidance or none), xgrammar when it was not given, and none under
--skip-tokenizer-init, since the scheduler builds no grammar backend without
its tokenizer. The Rust servicer refuses a constraint that backend cannot
enforce before the scheduler, as the vLLM servicer does: every kind under
none (the scheduler itself would admit the request and abort it with a 400),
an EBNF grammar or a structural tag under outlines, which compiles JSON
schemas and regular expressions only, and a JSON-schema feature xgrammar drops
under xgrammar; a backend it does not know (a plugin of the scheduler's) is
left to the engine.
KV-event recovery
To retain cache knowledge across a recoverable event gap, configure SGLang's
--kv-events-config with both a PUB endpoint and a replay endpoint, for example:
{"publisher":"zmq","endpoint":"tcp://*:5557","replay_endpoint":"tcp://*:5558","buffer_steps":10000}
The bridge subscribes to live events before requesting missed batches from the
replay endpoint, preserves publisher sequence numbers, and removes overlap at
handoff. Both subscriptions currently use DP rank 0; allocate non-overlapping
port ranges if multiple DP ranks publish events. The Rust servicer's relay
uses the same replay endpoint (the vLLM, SGLang and TokenSpeed launchers all
pass replay_endpoint from the engine's kv-events config) for gaps in flight
and for the batches published before its subscription joined the publisher;
without one those are counted as lost or unknown, never silently skipped. The
relay dials the publisher every second from the servicer's start, so its
subscription lands within a second of the engine binding the socket, and the
launcher starts its planned warm-up only after that, so the warm-up's batches
(the engine's first) are the first the relay's history records.
Without replay, or when history is expired, empty, malformed, or unavailable
(timeout: five seconds), the bridge reports OUT_OF_RANGE before streaming or
DATA_LOSS after streaming starts. SMG discards that worker's stale mappings and
resubscribes with zero. A zero cursor rebuilds knowledge from subsequent live
events; it is not a complete cache snapshot: this bridge relays per call and
keeps no history or block record between calls, so it has nothing older to
serve. The Rust servicer's relay (crates/engine_servicer, the vLLM servicer
and SMG_SGLANG_SERVICER_IMPL=rust) does: it keeps a bounded history and the
engine's live blocks for the servicer's lifetime, serves the history to a
subscription from zero while it is complete, and a state snapshot
(KvSnapshotChunk) before live events once the window has rolled. An empty
replay is conservatively reset because it cannot distinguish an idle publisher
from a restarted one.
Rust request path (--servicer-impl rust)
sglang serve --grpc-mode (python -m sglang.launch_server --grpc-mode)
hands the process to this package's serve_grpc. With --servicer-impl rust
that entry serves the sglang.grpc.scheduler.SglangScheduler contract from
Rust (smg.servicer.SglangGrpcServer, which needs the smg wheel) instead:
the scheduler runs headless in a spawned child over the msgpack ZMQ wire
below, the Rust server speaks the gRPC contract on top of it, and Python keeps
the lifecycle only. The flag is this package's, as it is for vLLM: SGLang
loads it as an sglang.srt.plugins entry point while it builds its parser
(smg_grpc_servicer/sglang/plugin.py), and SMG_SGLANG_SERVICER_IMPL is the
fallback the flag overrides (flag, then the environment, then python; the
servicer logs which one decided). A SGLANG_PLUGINS whitelist without
smg-servicer removes the flag, not the environment form.
# Python (default)
sglang serve --model-path Qwen/Qwen3-0.6B --grpc-mode --port 30000
# Rust request path, same entrypoint
sglang serve --model-path Qwen/Qwen3-0.6B --grpc-mode --port 30000 --servicer-impl rust
# The environment form, overridden by the flag when both are given
SMG_SGLANG_SERVICER_IMPL=rust sglang serve --model-path Qwen/Qwen3-0.6B --grpc-mode --port 30000
python -m sglang.launch_server --help lists the flag; sglang serve --help
prints SGLang's help before it loads plugins, so the flag is missing from that
listing but accepted. The Router cannot tell the two implementations apart: text
generation with sampled and prompt logprobs, reasoning-token counts, LoRA ids
and custom logit processors forwarded as the Python servicer forwards them,
Embed, FlushCache and profiling (the scheduler's own control requests,
carried by the wire's control call) all answer as the Python servicer does.
Hidden states (return_hidden_states) are served on this path, and on this
path only: the final hidden layer's vector per position a forward ran on (the
prompt positions the prefill computed, then each generated token fed back:
(prompt - cached prefix) + generated - 1 rows, with absolute positions anchored
from the engine's counters), the prefill block on the first chunk, a decode
step's rows on each later one and the whole list on the completion, within the
scheduler's --return-hidden-states-mode cap, which both servicers keep ahead
of the scheduler with the engine's own words; the Python servicer then refuses
the request by name (INVALID_ARGUMENT, pointing at --servicer-impl rust),
where it once accepted the flag and returned nothing.
Prefill/decode disaggregation (--disaggregation-mode prefill|decode) works as
on the Python servicer: a request's bootstrap fields (host, port, room) ride
the scheduler's own request struct over the wire, the prefill side's bootstrap
server starts in the servicer process before the ranks, and GetServerInfo
carries the server args the Router pairs on. Multimodal inputs reach the
scheduler as its own processor output: the preprocessed tensors the Router's
media pipeline sends ride the wire as SGLang's own tensor encoding, and the
scheduler hashes, pads and positions them as it does behind the Python
servicer; supports_vision is advertised as SGLang's model config decides it.
The language side of encode/prefill disaggregation (--language-only) works as
behind SGLang's own frontend: the servicer process runs SGLang's own encoder
bootstrap server and receiver (--encoder-bootstrap-port, --encoder-urls,
the receiver's transport by SGLANG_ENCODER_MM_RECEIVER_MODE), a request's
image references (mm_inputs.image_urls, with the prompt tokenized but not
expanded and no tensors) go to the encoders through them before the request
reaches the scheduler, the scheduler's request carries what the dispatch set
(its wait flag, the per-encoder assignment, the encoder list) and the prompt
text the scheduler rebuilds from once the embeddings arrive, and a dispatch
that fails afterwards reaches the scheduler as its own failure notice. A
request of references on any other worker, or on a language-only worker with
no encoder registered, is refused (FAILED_PRECONDITION) rather than answered
as text. A request the Router pins to a data-parallel rank (--dp-size above one
behind a dp-aware Router) goes to that rank's scheduler, as the engine's
data-parallel controller sends it behind SGLang's own frontend; a pin past the
connected ranks is refused as the caller's error. On the prefill side of a disaggregated pair a request runs on
the rank of its room's residue instead (bootstrap_room % dp_size: SGLang's
prefill side follows the room and its KV sender fails a request placed
elsewhere, and the decode side looks for the KV on that rank), the Router's pin
yielding to it with a warning; the decode side keeps the pin. A prefill leg of
several plain data-parallel replicas (--dp-size N without attention DP) is
refused at launch with the engine's reason: every replica reports attention-DP
rank 0 to the engine's KV sender, which fails each request whose room's residue
is not 0; attention DP (--tp-size N --attn-dp-size N) is the multi-rank
prefill shape. The planned
warm-up pins each of its requests to a rank (on a prefill or decode leg with a
stand-in room of that rank's residue), so every rank is warm before the Router
sends anything. What the wire does not carry is reported, not emulated: an --encoder-only launch is refused (SGLang's entrypoint runs
that role on its own encoder server, never on a servicer), a --language-only
launch on the zmq_to_tokenizer transfer backend is refused (the frontend
itself receives and processes the embeddings there, which neither servicer
runs), and LoRA loading answers UNIMPLEMENTED, as it does on the Python
servicer.
SubscribeKvEvents relays SGLang's ZMQ KV-event publisher, which the servicer
turns on by itself, on this path as on the Python one, when the launcher is
given no --kv-events-config, at a port found free on the host rather than
SGLang's fixed default (an explicit one is kept as given, its endpoint
included; SMG_SGLANG_SERVICER_KV_EVENTS=0 leaves the publisher off). SGLang's
HTTP sidecar (metrics and profiling endpoints) is not started on this path.
Tuning: SMG_SGLANG_SERVICER_HANDSHAKE_PORT (default: a
free port), SMG_SGLANG_SERVICER_DRAIN_SECS (default 5),
SMG_SGLANG_SERVICER_WARMUP (the planned warm-up before SERVING, as for vLLM
above: full by default, fast, off, a budget in seconds or
<level>:<seconds>; planned from --max-running-requests and
--chunked-prefill-size or --max-prefill-tokens, the engine's defaults
where unset; the Python servicer runs the same plan over its own port after
its first warm-up request and skips it on a disaggregation role; the Rust
path plans a prefill or decode leg as a single engine, its requests carrying
the engine's stand-in bootstrap so the leg warms up without its peer, and the
language side of an encode-disaggregated deployment (--language-only) plans
as a single engine, so the first forward of its fresh scheduler is paid before
SERVING rather than by its first request, as the engine's own frontend warms
that role on the text path, and where its
frontend dispatches to the encoders the plan ends its shapes with one image
through them (the prompt the model's chat template renders around the
engine's own minimal warm-up image; a template without an image placeholder
leaves the leg out, a leg the encoders cannot serve yet is a logged step), so
the scheduler's first media forward is paid before SERVING too; under
data parallelism the servicer routes a request by load, not by rank, so each
prefill shape warms one rank and the log says so),
SMG_SGLANG_SERVICER_STARTUP_TIMEOUT_SECS (default 1800) and
SMG_SGLANG_SERVICER_STARTUP_CEILING_SECS (default 14400; the two startup
bounds as for vLLM above: the launcher reports the scheduler process alive each
time it polls it, so a slow but living start completes, under the ceiling, and
0 lifts the ceiling), SMG_SGLANG_SERVICER_SILENCE_TIMEOUT_SECS (default 300)
and SMG_SGLANG_SERVICER_PREFILL_TOKENS_PER_SEC (default 2000; the connected
scheduler's silence bound and its prefill allowance as for TokenSpeed above:
this wire too carries token batches only), SMG_ZMQ_SOCKET_DIR,
SMG_SERVICER_WORKER_THREADS.
Metrics (--metrics-port)
With --servicer-impl rust, --metrics-port <port> (and --metrics-host,
defaulting to the gRPC --host) serves the Rust servicer's Prometheus metrics
on a listener of its own; SMG_SGLANG_SERVICER_METRICS_PORT /
SMG_SGLANG_SERVICER_METRICS_HOST are the environment forms the flags
override, and without either there is no endpoint. The flags are this package's
(the same plugin that adds --servicer-impl); a SGLANG_PLUGINS whitelist
without smg-servicer removes them, not the environment form. The families
and labels are the ones every engine's Rust servicer exposes (see the vLLM
section above); the scheduler matches string stops itself, so this servicer
has no smg_servicer_string_stops_total series. A port equal to --port is
refused before the scheduler starts, and the flag is refused under the Python
servicer, which exposes no endpoint (SGLang's own HTTP sidecar is not started
on the Rust path either).
sglang serve --model-path Qwen/Qwen3-0.6B --grpc-mode --port 30000 --servicer-impl rust --metrics-port 29100
Headless over ZMQ (no SGLang change)
SMG can drive SGLang's scheduler directly over ZMQ, the same same-host lane it has for vLLM and TokenSpeed, without a gRPC server or SGLang's tokenizer manager in between:
python -m smg_grpc_servicer.sglang.headless \
--zmq-handshake-address tcp://127.0.0.1:<port> --zmq-engine-index 0 \
--model-path Qwen/Qwen3-0.6B [any SGLang server args]
smg serve --backend sglang --connection-mode zmq runs exactly this for each
worker, with the handshake port SMG derives from the worker's ipc:// URL.
SMG's wait for the scheduler's HELLO, and for the READY and the registration
that follow it, is bounded per stage by the router's
--zmq-startup-timeout-secs (default 3600 s; --router-zmq-startup-timeout-secs
from smg serve): the scheduler dials in only after its model is loaded, so the
bound has to cover that load.
The launcher spawns the scheduler ranks the way SGLang's engine does; the
scheduler keeps its tokenizer (SMG tokenizes prompts, but SGLang's grammar
backend for constrained decoding only exists alongside a tokenizer). Inside
each rank, SGLang loads
this package's plugin (smg_grpc_servicer.sglang.zmq_plugin, registered under
the sglang.srt.plugins entry point and inert unless the launcher's
SMG_SGLANG_ZMQ_HANDSHAKE is set), which hooks the scheduler's own ingress
and egress seams: the rank that owns request I/O dials SMG's handshake once
its model is loaded, registers its geometry, decodes SMG's requests (the
scheduler's native TokenizedGenerateReqInput or TokenizedEmbeddingReqInput,
normalized and verified as the tokenizer manager would, and control calls
that become the scheduler's own FlushCacheReqInput or ProfileReq) and
answers with slim positional structs: a per-step token batch (token ids,
finish reason with message, matched stop and abort status, counts including
reasoning tokens, sampled-token and prompt logprobs with ranked top logprobs
when asked, and a scheduler-load tail), an embedding batch, and a control
reply under the call id. Requests the scheduler cannot serve, n > 1 for
example (SMG fans out itself), are answered with a terminal abort rather than
dropped. SGLang itself is unchanged. The plugin declares the field order of
the structs it decodes in its ready response, and the Rust servicer checks the
prefixes it encodes against it once, at the handshake: a release that moves a
field inside them fails the engine link with the struct and the field named,
instead of refusing every request. The sampling parameters go out as their API
prefix (through custom_params), so a release that adds a field after it
decodes them unchanged; a struct change upstream otherwise shows up in
grpc_servicer/tests/test_sglang_zmq_msgpack.py.
Multi-node engines: the worker pod's liveness
An engine whose tensor-parallel ranks span pods is launched on every pod with
the same flags, --nnodes N --dist-init-addr <the leader>:<port> among them,
and the pod's own --node-rank <k>: the gRPC servicer (sglang serve --grpc-mode) or the headless launcher above on the leader pod (--node-rank 0), the scheduler ranks alone on the worker pods. Every rank joins the
process group through the TCP store at --dist-init-addr, which rank 0 on
the leader pod hosts, and keeps that connection for its lifetime, so the
contract of the vLLM section above holds here too: a leader restart must
restart its workers, and python -m smg_grpc_servicer.sglang.worker_probe is
the worker pod's probe. It passes while at least one TCP connection from the
pod to the leader's store port is established and fails otherwise; without
arguments it reads the leader and the port off --dist-init-addr on the
worker's own command line (/proc/1/cmdline), --leader <name> names the
leader by its stable DNS name instead, re-resolved on every run, and --port
names the store port. Run it with python3 -I and a timeoutSeconds of 30
or more, as above:
# the worker pod's container; LEADER_HOST is the leader's stable DNS name
startupProbe:
exec: {command: [/bin/sh, -c, 'python3 -I -m smg_grpc_servicer.sglang.worker_probe --leader "$LEADER_HOST"']}
periodSeconds: 30
timeoutSeconds: 30
failureThreshold: 480
livenessProbe:
exec: {command: [/bin/sh, -c, 'python3 -I -m smg_grpc_servicer.sglang.worker_probe --leader "$LEADER_HOST"']}
periodSeconds: 30
timeoutSeconds: 30
failureThreshold: 3
Architecture
smg-grpc-servicer[vllm] ──optional dep──> vllm (lazy import)
smg-grpc-servicer[mlx] ──optional dep──> mlx-lm (lazy import)
smg-grpc-servicer ──external runtime──> tokenspeed (lazy import)
smg-grpc-servicer[sglang] ──optional dep──> sglang (lazy import)
smg-grpc-servicer ──depends on────> smg-grpc-proto (hard dependency)
vllm ──optional──────> smg-grpc-servicer (via vllm serve --grpc)
sglang ──optional──────> smg-grpc-servicer (via --grpc-mode)
Backend dependencies are isolated via extras or runtime installs to avoid conflicts between vLLM, MLX, TokenSpeed, and SGLang.
Development
See DEVELOPMENT.md for local development setup, CI, and release workflows.
Metadata
Release files for smg-grpc-servicer 0.14.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| smg_grpc_servicer-0.14.1.tar.gz | 726.9 kB | Without provenance | Details for smg_grpc_servicer-0.14.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| smg_grpc_servicer-0.14.1-py3-none-any.whl | Python 3 | none | any | Details for smg_grpc_servicer-0.14.1-py3-none-any.whl |
Total release size: 1.1 MB
Release files / smg_grpc_servicer-0.14.1.tar.gz
| Download URL | smg_grpc_servicer-0.14.1.tar.gz |
|---|---|
| Size | 726.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
286ac8eb842560c4fdc8c5af1275f09b7eab3b228068065b3079be18604b00e2
|
|
BLAKE2b-256 checksum How to use checksums |
9b99a5283b73ea420cd375781b206938f050f9daccef9e30dccabdbfa1e868c3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.15
|
Release files / smg_grpc_servicer-0.14.1-py3-none-any.whl
| Download URL | smg_grpc_servicer-0.14.1-py3-none-any.whl |
|---|---|
| Size | 411.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0d78a7e38b7cf5f7f1b044051f6795b3ca1b289ef0b44eaf1945b0b13703e2e8
|
|
BLAKE2b-256 checksum How to use checksums |
4d988041bd27b1fd98e3a69a92693138bf1083a67f33b658d328878e1c5e7a57
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.15
|
Security for smg-grpc-servicer 0.14.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.