Skip to content

feat(mlx): add Python gRPC servicer for MLX backend - #1099

Merged
slin1237 merged 23 commits into
mainfrom
keyang/mlx-python
Apr 27, 2026
Merged

slin1237 merged 23 commits into
mainfrom
keyang/mlx-python

Conversation

@key4ng

@key4ng key4ng commented Apr 11, 2026 •

Copy link
Copy Markdown
Member

Description

Problem

#1034 added the MlxEngine proto and Rust client. This PR adds the corresponding Python servicer that actually runs mlx-lm on Apple Silicon, completing the end-to-end pipeline.

Solution

A new grpc_servicer/smg_grpc_servicer/mlx/ module implementing the MlxEngine proto service. Wraps mlx-lm's BatchGenerator behind the gRPC contract from PR #1034, enabling SMG to route requests to Apple Silicon workers the same way it routes to vLLM/SGLang/TRT-LLM.

Changes

Python servicer (grpc_servicer/smg_grpc_servicer/mlx/)

  • MlxEngineServicer — implements all 8 RPCs. Generate supports both streaming and non-streaming. A background daemon thread runs BatchGenerator.next() in a tight loop and dispatches tokens to per-request asyncio.Queues via loop.call_soon_threadsafe.
  • MlxHealthServicer — standard grpc.health.v1.Health protocol for liveness/readiness probes.
  • server.py — CLI entrypoint with model loading, warmup, graceful shutdown. Run via python -m smg_grpc_servicer.mlx.server --model <path>.

Sampling parameter mapping

Proto field mlx-lm API
temperature, top_p, top_k, min_p make_sampler(...)
repetition_penalty, frequency_penalty, presence_penalty, logit_bias make_logits_processors(...)
max_tokens, stop_token_ids, ignore_eos BatchGenerator.insert + SequenceStateMachine
logprobs Top-k extraction from generate_step logprobs array
seed mx.random.seed() (best-effort, global state)

Packaging

  • New mlx optional dependency in grpc_servicer/pyproject.toml: install with pip install smg-grpc-servicer[mlx]
  • smg-grpc-proto Python package exports mlx_engine_pb2 / mlx_engine_pb2_grpc

E2E test (e2e_test/chat_completions/test_mlx_backend.py)

5 tests exercising the full SMG router → gRPC → MLX pipeline:

  • basic chat completion
  • streaming chat
  • tool calling via SMG's qwen ToolParser (parses <tool_call> tags)
  • reasoning content via Qwen3 thinking mode
  • max_tokens finish reason

Uses mlx-community/Qwen3-0.6B-4bit (~400 MB). Skipped on non-Darwin/non-arm64 via module-level pytestmark.

Infrastructure additions:

  • Runtime.MLX enum + is_mlx() helper in e2e_test/infra/constants.py
  • _build_mlx_cmd() in e2e_test/infra/worker.py (gRPC-only, rejects HTTP mode)
  • Model spec entry for mlx-community/Qwen3-0.6B-4bit

CI (.github/workflows/pr-test-mlx.yml)

Runs the E2E test on macos-14 (Apple Silicon) GitHub-hosted runners.

  • Triggers only on MLX-related path changes
  • Caches Rust target (via Swatinem/rust-cache) and HF model (~/.cache/huggingface/hub)
  • Uses debug maturin build (correctness test, not perf test — saves ~5 min vs release)
  • Estimated: first run ~12 min, cached runs ~4-5 min

Test Plan

Verified locally on Apple Silicon with mlx-community/Qwen3-0.6B-4bit: 15/15 test passes across 3 consecutive runs. Also verified earlier with mlx-community/Qwen3-8B-4bit for production-quality output.

# Local run
E2E_RUNTIME=mlx pytest e2e_test/chat_completions/test_mlx_backend.py -v
Checklist
  • cargo +nightly fmt passes
  • cargo clippy --all-targets --all-features -- -D warnings passes
  • E2E test verified locally
  • (Optional) Documentation updated
  • (Optional) Please join us on Slack #sig-smg to discuss, review, and merge PRs

Summary by CodeRabbit

  • New Features

    • MLX backend added for inference serving with runtime detection, model spec for mlx-community/Qwen3-0.6B-4bit, MLX-aware gRPC server and health reporting, and optional MLX dependencies.
  • Tests

    • New Apple Silicon–only end-to-end MLX test suite covering streaming/non-streaming chat, tool-calling, reasoning/thinking behavior, and token-limit finish reasons.
  • Chores

    • CI workflow to run MLX E2E tests on macOS for PRs and pushes.

key4ng added 5 commits April 10, 2026 21:42
Implements MlxEngineServicer (8 RPCs) backed by mlx-lm's BatchGenerator
for Apple Silicon inference. Wraps the BatchGenerator behind the
MlxEngine proto from #1034.

Components:
- MlxEngineServicer: Generate (streaming + non-streaming), HealthCheck,
  Abort, GetModelInfo, GetServerInfo, GetTokenizer; Embed and
  SubscribeKvEvents return UNIMPLEMENTED
- MlxHealthServicer: standard grpc.health.v1.Health protocol
- server.py: standalone gRPC server entrypoint with model loading
- 42 unit + integration tests covering all RPCs

Generation loop:
- Background daemon thread runs BatchGenerator.next() in tight loop
- Dispatches tokens to per-request asyncio.Queues via
  loop.call_soon_threadsafe
- Auto-removes completed/aborted requests from BatchGenerator

Sampling param mapping:
- temperature, top_p, top_k, min_p -> make_sampler()
- repetition/frequency/presence_penalty, logit_bias ->
  make_logits_processors()
- max_tokens, stop_token_ids (via SequenceStateMachine), ignore_eos
- logprobs (output only, top-k extraction from generate_step logprobs)
- seed (best-effort, mx.random.seed global state)

Also exports mlx_engine_pb2/mlx_engine_pb2_grpc from smg-grpc-proto
package and adds [mlx] optional dependency to grpc_servicer.

Signed-off-by: key4ng <rukeyang@gmail.com>
vllm and sglang servicers have no unit tests in this repo — testing
is done via E2E pytest in e2e_test/. Remove MLX unit tests for
consistency.

Signed-off-by: key4ng <rukeyang@gmail.com>
Adds Apple-Silicon-only E2E test using mlx-community/Qwen3-0.6B-4bit
(~400 MB) which fits comfortably on a macos-latest GitHub Actions
runner. Qwen3 supports both native tool calling and thinking mode in
a single small model.

Test coverage:
- Basic non-streaming chat completion
- Streaming chat with delta chunks
- Tool calling via SMG's qwen ToolParser parsing <tool_call> tags
- Reasoning content via thinking mode
- max_tokens finish_reason

Infrastructure additions:
- Runtime.MLX enum + is_mlx() helper
- _build_mlx_cmd() in worker.py (gRPC-only, no HTTP variant)
- mlx-community/Qwen3-0.6B-4bit entry in MODEL_SPECS

Tests skip on non-Darwin/non-arm64 platforms via module-level pytestmark.
Verified locally: 15/15 passes across 3 runs.

Signed-off-by: key4ng <rukeyang@gmail.com>
Adds .github/workflows/pr-test-mlx.yml that runs the MLX E2E test on
macos-14 (Apple Silicon) GitHub-hosted runners. Triggered on changes to
MLX-related paths (proto, Rust client, Python servicer, MLX test file,
or the workflow itself).

Steps:
1. Install protoc, Rust toolchain, Python 3.12
2. Cache Cargo + HuggingFace model
3. Build smg-grpc-proto Python package (proto codegen)
4. Install grpc_servicer[mlx] (pulls mlx + mlx-lm wheels)
5. Build smg Python bindings via maturin develop --release
6. Generate Python client types (cargo + datamodel-code-generator)
7. Install smg-client + e2e_test (deps)
8. Verify imports
9. Run pytest with E2E_RUNTIME=mlx, --reruns 1 for flake tolerance
10. Upload logs on failure

The test downloads mlx-community/Qwen3-0.6B-4bit (~400 MB) on first
run and caches it for subsequent runs.

Signed-off-by: key4ng <rukeyang@gmail.com>
--release build takes 6-8 min on macos-14 3-core runners. Since this
is a correctness test (not a perf test), debug is sufficient and
drops first-run time to ~7 min, cached runs to ~3 min.

Signed-off-by: key4ng <rukeyang@gmail.com>
@github-actions github-actions Bot added dependencies Dependency updates ci CI/CD configuration changes grpc gRPC client and router changes tests Test changes labels Apr 11, 2026
@coderabbitai

coderabbitai Bot commented Apr 11, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds MLX backend support across CI, proto re-exports, e2e infra (runtime/model/worker), a new MLX gRPC servicer (health, server, engine), packaging extras, and macOS arm64 E2E tests.

Changes

Cohort / File(s) Summary
CI / Workflows
.github/workflows/pr-test-mlx.yml
New GitHub Actions workflow to run MLX E2E on macOS (Python/Rust setup, protobuf, build/install wheels, cache HF models, run pytest, upload e2e-logs on failure).
gRPC Client proto exports
crates/grpc_client/python/smg_grpc_proto/__init__.py
Re-exports MLX proto modules (mlx_engine_pb2, mlx_engine_pb2_grpc) and adds them to __all__.
E2E Infra & models
e2e_test/infra/constants.py, e2e_test/infra/model_specs.py, e2e_test/infra/worker.py, e2e_test/infra/__init__.py
Adds Runtime.MLX, is_mlx() predicate and label, includes MLX in LOCAL_RUNTIMES, adds MLX model spec, worker _build_mlx_cmd (enforces GRPC mode), and re-exports is_mlx.
gRPC Servicer packaging & exports
grpc_servicer/pyproject.toml, grpc_servicer/smg_grpc_servicer/mlx/__init__.py, grpc_servicer/smg_grpc_servicer/mlx/__main__.py
Adds mlx optional dependencies, creates MLX servicer package exports and module entrypoint.
MLX Health servicer
grpc_servicer/smg_grpc_servicer/mlx/health_servicer.py
New MlxHealthServicer implementing Check/Watch, serving flag management, and service-name handling (OVERALL_SERVER / MLX_SERVICE).
MLX gRPC server entrypoint
grpc_servicer/smg_grpc_servicer/mlx/server.py
New server CLI and async serve logic: model loading/snapshot, EOS handling, warmup, gRPC server setup, health wiring, graceful shutdown.
MLX Engine servicer
grpc_servicer/smg_grpc_servicer/mlx/servicer.py
New MlxEngineServicer with request bookkeeping, generation loop, streaming & non-streaming Generate, Abort, GetModelInfo/ServerInfo, GetTokenizer, and health behavior.
E2E Tests (macOS arm64 only)
e2e_test/chat_completions/test_mlx_backend.py
New Apple Silicon–only E2E tests covering basic chat, streaming, tool-calling, reasoning/thinking, and max-tokens finish reason; class uses MLX engine/model and MLX-specific gateway args.

Sequence Diagram

sequenceDiagram
    autonumber
    participant Client as Client / Test
    participant Router as SMG Router
    participant Servicer as MlxEngineServicer
    participant BatchGen as BatchGenerator
    participant MLX as MLX Model
    participant Health as MlxHealthServicer

    Client->>Router: Send request
    Router->>Servicer: gRPC Generate(request)
    Servicer->>Servicer: Validate request & build sampler/logits
    Servicer->>BatchGen: Insert request (uid)
    Servicer->>Health: Ensure serving state
    loop generation batches
      BatchGen->>MLX: next() -> batch
      MLX->>BatchGen: Token results / finish state
      BatchGen->>Servicer: Route per-uid results
      Servicer->>Client: Stream GenerateStreamChunk(s) / final GenerateComplete
    end
    Servicer->>BatchGen: Cleanup finished uid
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

python-bindings

Suggested reviewers

  • CatherineSue
  • slin1237

Poem

🐰 I hopped through code and built a test,
MLX now wakes and does its best,
Servicers hum and tokens dart,
Apple silicon plays its part,
Hooray — another gateway quest!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.34% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely identifies the main change: adding a Python gRPC servicer for MLX backend support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch keyang/mlx-python

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new MLX inference backend for Apple Silicon, featuring a gRPC servicer, a standalone server entry point, and comprehensive E2E tests using the Qwen3 model. The implementation leverages the mlx-lm library for batch generation. The review feedback primarily addresses critical thread-safety concerns, as the BatchGenerator is accessed concurrently by the background generation thread and gRPC tasks without synchronization. Other recommendations include fixing hardcoded token usage counts in streaming responses, using more robust event loop retrieval methods, and following standard Python practices for import placement.

Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py Outdated
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py Outdated
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py Outdated
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py Outdated
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py Outdated
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py Outdated
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py Outdated
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work — clean implementation of the MLX gRPC servicer. The architecture (event loop for insert, background daemon thread for BatchGenerator.next(), call_soon_threadsafe dispatch) is sound and matches the design from PR #1034.

Review summary:

  • 🔴 Important: 0
  • 🟡 Nit: 3
  • 🟣 Pre-existing: 0

The nits flagged are: (1) batch_generator.remove() called from both the event-loop thread (Abort) and the generation thread — potential thread-safety concern, (2) _build_output_logprobs with num_logprobs=0 returns entire vocabulary due to [-0:] semantics, (3) asyncio.get_event_loop() deprecated in favor of get_running_loop().

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ee5cce360

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py Outdated
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py
…/format

- Use 'maturin build --out dist && pip install dist/*.whl' instead of
  'maturin develop' — GitHub-hosted runner Python is not in a venv,
  so 'maturin develop' errors out with 'Couldn't find a virtualenv'.
- Fix ruff E731: replace lambda with def _stream_ctx in generation loop
- Apply ruff format to all mlx python files

Signed-off-by: key4ng <rukeyang@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@grpc_servicer/smg_grpc_servicer/mlx/server.py`:
- Around line 116-118: The code currently ignores the return value of
server.add_insecure_port(listen_addr) so the process can continue even if the
socket failed to bind; update the startup in server.py to capture the return
(e.g., bound_port = server.add_insecure_port(listen_addr)) and if it is 0, log
an error and fail fast (raise an exception or call sys.exit(1)) before calling
await server.start(), ensuring you reference listen_addr,
server.add_insecure_port, and await server.start() in your change.

In `@grpc_servicer/smg_grpc_servicer/mlx/servicer.py`:
- Around line 89-104: In _build_output_logprobs: guard the case num_logprobs ==
0 (or top_k <= 0) before computing top_k and slicing with [-top_k:] to avoid
selecting the whole vocabulary; if num_logprobs is 0 return None (or an empty
OutputLogProbs) early, otherwise proceed to compute top_k, top_indices =
mx.argpartition(...)[-top_k:], top_values, sort_order, etc., so logprobs_array
and the top-k logic are only run when top_k > 0.
- Around line 374-383: Abort currently removes uid mappings but never signals
the waiting Generate coroutine, causing it to hang on queue.get(); modify Abort
(mlx_grpc_servicer.ml x.servicer Abort) to look up the uid via
self._request_uid_map and, when removing self._uid_queues.pop(uid, None), put a
terminal sentinel (e.g., None) into the corresponding queue (or call
put_nowait(None)) so any blocked Generate waiter wakes; also update Generate to
check the dequeued value (r = await queue.get()) for the sentinel (if r is None:
break/return) before accessing r.token so the stream can finish and
_active_requests can decrement properly.
- Around line 194-210: The GetModelInfoResponse currently recomputes
eos_token_ids from config and returns [] when config lacks eos_token_id; instead
reuse the resolved EOS IDs that the server already computes (the
tokenizer-derived fallback implemented in server.py lines ~56-64). Update the
code that builds GetModelInfoResponse to use the pre-resolved EOS ID container
(e.g., the attribute or variable the server module exposes such as the
resolved_eos_ids / self.eos_token_ids or the same helper used in server.py)
rather than recomputing from config, so the response matches the actual
generation stop behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 13fbb568-26da-40b8-b8a6-fe0814769626

📥 Commits

Reviewing files that changed from the base of the PR and between 381c5f6 and 6ee5cce.

📒 Files selected for processing (13)
  • .github/workflows/pr-test-mlx.yml
  • crates/grpc_client/python/smg_grpc_proto/__init__.py
  • e2e_test/chat_completions/test_mlx_backend.py
  • e2e_test/infra/__init__.py
  • e2e_test/infra/constants.py
  • e2e_test/infra/model_specs.py
  • e2e_test/infra/worker.py
  • grpc_servicer/pyproject.toml
  • grpc_servicer/smg_grpc_servicer/mlx/__init__.py
  • grpc_servicer/smg_grpc_servicer/mlx/__main__.py
  • grpc_servicer/smg_grpc_servicer/mlx/health_servicer.py
  • grpc_servicer/smg_grpc_servicer/mlx/server.py
  • grpc_servicer/smg_grpc_servicer/mlx/servicer.py

Comment thread grpc_servicer/smg_grpc_servicer/mlx/server.py Outdated
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py Outdated
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 17389ce93a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
grpc_servicer/smg_grpc_servicer/mlx/servicer.py (3)

201-228: ⚠️ Potential issue | 🟡 Minor

Reuse the resolved EOS IDs for consistency.

GetModelInfo recomputes eos_token_ids from config only (lines 208-214), returning [] when config lacks eos_token_id. However, _build_state_machine uses self._eos_token_ids which includes the tokenizer fallback computed in server.py. This inconsistency means GetModelInfo can report different EOS IDs than what generation actually uses.

🔧 Proposed fix
     async def GetModelInfo(
         self,
         request: mlx_engine_pb2.GetModelInfoRequest,
         context: grpc.aio.ServicerContext,
     ) -> mlx_engine_pb2.GetModelInfoResponse:
         config = self.model_config
-
-        eos = config.get("eos_token_id")
-        if isinstance(eos, int):
-            eos_token_ids = [eos]
-        elif isinstance(eos, list):
-            eos_token_ids = eos
-        else:
-            eos_token_ids = []

         return mlx_engine_pb2.GetModelInfoResponse(
             model_path=self.model_path,
             is_generation=True,
             max_context_length=config.get("max_position_embeddings", 0),
             vocab_size=config.get("vocab_size", 0),
             served_model_name=self.model_path,
             model_type=config.get("model_type", ""),
             architectures=config.get("architectures", []),
-            eos_token_ids=eos_token_ids,
+            eos_token_ids=self._eos_token_ids,
             pad_token_id=config.get("pad_token_id") or 0,
             bos_token_id=config.get("bos_token_id") or 0,
             max_req_input_len=config.get("max_position_embeddings", 0),
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@grpc_servicer/smg_grpc_servicer/mlx/servicer.py` around lines 201 - 228,
GetModelInfo currently recomputes eos_token_ids from model_config, causing
mismatch with the tokenizer-resolved EOS IDs used by _build_state_machine;
update GetModelInfo to use the already-resolved self._eos_token_ids (or fall
back to converting config values only if self._eos_token_ids is empty) and
ensure the value passed (eos_token_ids) is a list of ints to preserve
consistency between reported EOS IDs and those used by generation.

401-410: ⚠️ Potential issue | 🔴 Critical

Abort must wake the blocked Generate waiter.

Generate() is blocked on queue.get(). When Abort pops the queue from _uid_queues without putting a terminal sentinel, the Generate coroutine hangs indefinitely, _active_requests never decrements, and resources leak.

🔧 Proposed fix
     async def Abort(self, request, context):
         for request_id in request.request_ids:
             uid = self._request_uid_map.pop(request_id, None)
             if uid is not None:
-                self._uid_queues.pop(uid, None)
+                queue = self._uid_queues.pop(uid, None)
+                if queue is not None and self._loop is not None:
+                    self._loop.call_soon_threadsafe(queue.put_nowait, None)
                 try:
                     self.batch_generator.remove([uid])
                 except Exception:
                     logger.warning("Failed to remove uid %d for request %s", uid, request_id)
         return mlx_engine_pb2.AbortResponse()

Then handle the sentinel in Generate():

                 if request.stream:
                     while True:
                         r = await queue.get()
+                        if r is None:
+                            break
                         output_logprobs = self._build_output_logprobs(

(And similarly in the non-streaming branch.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@grpc_servicer/smg_grpc_servicer/mlx/servicer.py` around lines 401 - 410,
Abort currently pops entries from _uid_queues (via Abort) which leaves any
awaiting Generate coroutines blocked on queue.get(); modify Abort (function
Abort) to put a terminal sentinel into the corresponding per-uid queue after
removing the uid (or instead of removing) so any blocked Generate waiter is
unblocked, and ensure you still call batch_generator.remove([uid]) and pop from
_request_uid_map; then update Generate (function Generate) to detect the
sentinel from _uid_queues[uid] (and the non-streaming branch counterpart), treat
it as a termination signal by decrementing _active_requests and
returning/cleaning up instead of waiting indefinitely.

90-116: ⚠️ Potential issue | 🟠 Major

Guard num_logprobs <= 0 before computing top-k slice.

When num_logprobs is 0, top_k becomes 0 and [-0:] selects the entire vocabulary, causing unnecessary computation and payload bloat.

🔧 Proposed fix
     `@staticmethod`
     def _build_output_logprobs(token_id, logprobs_array, num_logprobs):
         """Build OutputLogProbs proto from an mlx logprobs array."""
-        if num_logprobs is None:
+        if num_logprobs is None or num_logprobs <= 0:
             return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@grpc_servicer/smg_grpc_servicer/mlx/servicer.py` around lines 90 - 116, The
_build_output_logprobs function should guard against non-positive num_logprobs
to avoid slicing with [-0:] which returns the entire vocab; modify
_build_output_logprobs to check if num_logprobs is None or num_logprobs <= 0 and
return None immediately, otherwise continue computing top_k and the
top_indices/top_values; update references inside _build_output_logprobs
(token_id, logprobs_array, num_logprobs, top_k, top_indices, top_values,
top_logprobs) accordingly so no further processing happens when num_logprobs <=
0.
grpc_servicer/smg_grpc_servicer/mlx/server.py (1)

131-134: ⚠️ Potential issue | 🟠 Major

Fail fast if the listen socket cannot be bound.

server.add_insecure_port(...) returns 0 when the address cannot be bound. Without checking this return value, the server proceeds into startup without actually listening.

🔧 Proposed fix
     listen_addr = f"{args.host}:{args.port}"
-    server.add_insecure_port(listen_addr)
+    bound_port = server.add_insecure_port(listen_addr)
+    if bound_port == 0:
+        raise RuntimeError(f"Failed to bind gRPC server to {listen_addr}")
     await server.start()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@grpc_servicer/smg_grpc_servicer/mlx/server.py` around lines 131 - 134, The
server currently ignores the return value of
server.add_insecure_port(listen_addr) which yields 0 on bind failure; update the
startup path in the block using listen_addr, server.add_insecure_port,
server.start and logger.info to check the returned port value, and if it's 0 log
an error via logger.error (including listen_addr), and fail fast (raise an
exception or exit) instead of calling await server.start() and logger.info.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e_test/chat_completions/test_mlx_backend.py`:
- Around line 104-128: The json import is inside test_tool_calling; move the
import statement (import json) to the module-level imports at the top of the
file and update the test to call json.loads(...) instead of _json.loads(...);
update references in test_tool_calling where call.function.arguments is parsed
to use json.loads(call.function.arguments) so the behavior and names match the
module-level import.

In `@grpc_servicer/smg_grpc_servicer/mlx/servicer.py`:
- Around line 241-253: start_generation_loop currently calls
asyncio.get_event_loop() which can be ambiguous when invoked from an async
context (serve_grpc); change this to use asyncio.get_running_loop() to retrieve
the active loop and assign it to self._loop, ensuring start_generation_loop and
the thread target _generation_loop use that running loop; update any related
tests or callers if they relied on get_event_loop behavior and consider a
fallback to asyncio.get_event_loop() only if get_running_loop() raises
RuntimeError in non-async call sites.

---

Duplicate comments:
In `@grpc_servicer/smg_grpc_servicer/mlx/server.py`:
- Around line 131-134: The server currently ignores the return value of
server.add_insecure_port(listen_addr) which yields 0 on bind failure; update the
startup path in the block using listen_addr, server.add_insecure_port,
server.start and logger.info to check the returned port value, and if it's 0 log
an error via logger.error (including listen_addr), and fail fast (raise an
exception or exit) instead of calling await server.start() and logger.info.

In `@grpc_servicer/smg_grpc_servicer/mlx/servicer.py`:
- Around line 201-228: GetModelInfo currently recomputes eos_token_ids from
model_config, causing mismatch with the tokenizer-resolved EOS IDs used by
_build_state_machine; update GetModelInfo to use the already-resolved
self._eos_token_ids (or fall back to converting config values only if
self._eos_token_ids is empty) and ensure the value passed (eos_token_ids) is a
list of ints to preserve consistency between reported EOS IDs and those used by
generation.
- Around line 401-410: Abort currently pops entries from _uid_queues (via Abort)
which leaves any awaiting Generate coroutines blocked on queue.get(); modify
Abort (function Abort) to put a terminal sentinel into the corresponding per-uid
queue after removing the uid (or instead of removing) so any blocked Generate
waiter is unblocked, and ensure you still call batch_generator.remove([uid]) and
pop from _request_uid_map; then update Generate (function Generate) to detect
the sentinel from _uid_queues[uid] (and the non-streaming branch counterpart),
treat it as a termination signal by decrementing _active_requests and
returning/cleaning up instead of waiting indefinitely.
- Around line 90-116: The _build_output_logprobs function should guard against
non-positive num_logprobs to avoid slicing with [-0:] which returns the entire
vocab; modify _build_output_logprobs to check if num_logprobs is None or
num_logprobs <= 0 and return None immediately, otherwise continue computing
top_k and the top_indices/top_values; update references inside
_build_output_logprobs (token_id, logprobs_array, num_logprobs, top_k,
top_indices, top_values, top_logprobs) accordingly so no further processing
happens when num_logprobs <= 0.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0689f1d8-d194-48e5-8540-3b4d09414aa8

📥 Commits

Reviewing files that changed from the base of the PR and between 6ee5cce and 17389ce.

📒 Files selected for processing (5)
  • .github/workflows/pr-test-mlx.yml
  • e2e_test/chat_completions/test_mlx_backend.py
  • grpc_servicer/smg_grpc_servicer/mlx/health_servicer.py
  • grpc_servicer/smg_grpc_servicer/mlx/server.py
  • grpc_servicer/smg_grpc_servicer/mlx/servicer.py

Comment thread e2e_test/chat_completions/test_mlx_backend.py Outdated
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py
Previous build used maturin's default (release) despite comment saying
debug. Switch to the ci profile already defined in bindings/python/
Cargo.toml (opt-level=2, thin LTO, 16 codegen-units) for faster
compilation on Apple Silicon runners.

Signed-off-by: key4ng <rukeyang@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/pr-test-mlx.yml:
- Line 43: The workflow job currently uses a floating runner image "runs-on:
macos-latest" which can drift; change that to an explicit Apple Silicon image
(e.g., "runs-on: macos-14") so the MLX/Apple-Silicon-specific job always runs on
an arm64 macOS host. Update the runs-on value in the job definition to the
pinned macOS 14 image and verify any matrix or conditionals that expect arm64
continue to work.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cd3db0f9-bee6-4296-8e35-de324e949e08

📥 Commits

Reviewing files that changed from the base of the PR and between 17389ce and f202dc2.

📒 Files selected for processing (1)
  • .github/workflows/pr-test-mlx.yml

Comment thread .github/workflows/pr-test-mlx.yml Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f202dc2849

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py Outdated
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py
- Default temperature to 1.0 when unset, matching vLLM/SGLang/TRT-LLM
  behavior (was forcing greedy decoding via 0.0).
- Guard logprobs=0 in _build_output_logprobs; top_k=0 previously caused
  [-0:] to slice the full vocabulary.
- Switch asyncio.get_event_loop() to get_running_loop() (deprecated in
  3.10+).
- Abort now puts a sentinel None into the request queue so a blocked
  Generate waiter unblocks instead of hanging until transport cancel.
- Streaming Generate emits a running completion_tokens counter on both
  chunks and the final GenerateComplete (was hardcoded to 1 / 0).
- Non-streaming Generate populates output_logprobs on GenerateComplete
  when sampling_params.logprobs is set.
- GetModelInfo now returns the resolved self._eos_token_ids (which
  falls back to tokenizer-derived IDs in server.py) instead of
  recomputing from config.json only.
- Add TODO(mlx-threadsafety) on the servicer class documenting the
  known BatchGenerator insert/next/remove multi-thread concern and
  deferring the fix (lock-around-next would kill throughput; needs a
  single-threaded request-queue refactor).

Signed-off-by: key4ng <rukeyang@gmail.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Thanks for the comprehensive update, @key4ng. The consolidation of fixes in 13c15206 and the clear documentation of deferred items and design decisions (like the BatchGenerator threading model) are very helpful. The approach to handling the Abort sentinel and the logprobs guard looks solid. I've noted the TODO(mlx-threadsafety) and the rationale for the current implementation. This looks ready for final review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@grpc_servicer/smg_grpc_servicer/mlx/servicer.py`:
- Around line 389-414: The code currently overwrites last_logprobs each
iteration, so when yielding the non-streaming response you must accumulate
per-token logprobs into a sequence-level payload instead of passing only the
final token's logprobs; change the loop that uses last_logprobs and
self._build_output_logprobs to collect each token's OutputLogProbs (e.g., create
output_logprobs_list = [] before the while, on each r call
self._build_output_logprobs(r.token, r.logprobs, num_logprobs) and append the
result to output_logprobs_list while handling possible None values), and then
pass the combined sequence-level logprobs (output_logprobs_list or an aggregated
structure expected by _complete_response) as output_logprobs to
self._complete_response along with all_output_ids, finish_reason, etc.; keep
variable names last_logprobs, _build_output_logprobs, _complete_response,
all_output_ids, and queue to locate the change.
- Around line 327-339: The background generator can observe a new uid from
batch_generator.insert before the per-uid asyncio.Queue is registered, causing
race drops; fix by serializing insert/next/remove with a shared lock (e.g.,
self._batch_lock) so uid visibility is atomic with queue publication: acquire
self._batch_lock before calling self.batch_generator.insert(...) in the handler,
create and assign self._uid_queues[uid] and self._request_uid_map[request_id],
then release the lock; likewise, acquire the same self._batch_lock around calls
to self.batch_generator.next(...) and any remove() logic inside _generation_loop
to ensure _generation_loop cannot observe a uid until its queue is registered.
Include the new lock initialization (e.g., in __init__) and use the same lock in
insert/next/remove paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5434ae22-7db1-4fd0-85f7-39606d6c8375

📥 Commits

Reviewing files that changed from the base of the PR and between f202dc2 and 13c1520.

📒 Files selected for processing (1)
  • grpc_servicer/smg_grpc_servicer/mlx/servicer.py

Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py Outdated
Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py Outdated
- server.py: check server.add_insecure_port return value (0 means bind
  failed) and raise instead of starting a server that never listened.
- servicer.py: in non-streaming Generate, aggregate per-token logprobs
  across the whole sequence into the final GenerateComplete instead of
  keeping only the last step's logprobs.

Signed-off-by: key4ng <rukeyang@gmail.com>
- GetModelInfo now reports self._ctx_limit (resolved via the 4-key
  fallback in __init__) instead of reading only
  max_position_embeddings. Previously reported 0 for model families
  that advertise context via max_seq_len / n_positions / seq_length.
- snapshot_download allow_patterns now includes tiktoken.model and
  *.tiktoken so tiktoken-only models can actually ship a usable
  tokenizer bundle via GetTokenizer (the earlier servicer whitelist
  fix only covered files that happened to be on disk).

Signed-off-by: key4ng <rukeyang@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 95866f6c45

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py
key4ng added 2 commits April 21, 2026 20:11
HF transformers>=4.43 ships chat_template as a standalone
chat_template.json / chat_template.jinja file for newer models (Gemma 4,
Llama 3.1+) instead of embedding it in tokenizer_config.json. Our
GetTokenizer whitelist dropped these, so the router-side
discover_chat_template_in_dir path found nothing and
ChatTemplateState::apply failed at chat-request time for affected models.

Fix in two places (same invariant as the tiktoken patch):
- _TOKENIZER_FILES in servicer.py includes both filenames.
- snapshot_download allow_patterns in server.py includes both so HF-repo
  model paths actually download them.

Qwen3 (our CI model) embeds chat_template in tokenizer_config.json and
would not have caught this; Gemma 4 is the canonical affected model.

Signed-off-by: key4ng <rukeyang@gmail.com>
Move the following out of PR #1099 to keep the review diff focused on
the servicer itself:
- e2e_test/chat_completions/test_mlx_backend.py (MLX E2E test suite)
- .github/workflows/pr-test-mlx.yml (CI workflow on macos-latest)
- MLX-specific additions to e2e_test/infra/{__init__,constants,model_specs,worker}.py

These will land in a follow-up PR from keyang/mlx-e2e-tests after this
servicer PR merges. The servicer code itself remains fully locally
testable via 'python -m smg_grpc_servicer.mlx.server --model ...'.

Signed-off-by: key4ng <rukeyang@gmail.com>
@github-actions github-actions Bot removed ci CI/CD configuration changes tests Test changes labels Apr 22, 2026
CI and e2e_test/infra/worker.py both invoke the servicer as
'python -m smg_grpc_servicer.mlx.server'. Nothing uses the shorter
'python -m smg_grpc_servicer.mlx' form that __main__.py enables.
Matches vllm/sglang conventions — neither ships a package-level
__main__.py either.

Signed-off-by: key4ng <rukeyang@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 000f74cc32

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

bound_port = server.add_insecure_port(listen_addr)
if bound_port == 0:
raise RuntimeError(f"Failed to bind gRPC server to {listen_addr}")
await server.start()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Delay accepting RPCs until generation loop is running

serve_grpc begins serving at await server.start() before _warmup(...) and servicer.start_generation_loop(), so Generate can accept requests in that startup window and then block forever on await queue.get() because no background generation thread is dispatching responses yet. This is practically reachable because the MLX client’s readiness path uses the service HealthCheck RPC (which always reports healthy), so traffic can be routed before the loop is initialized.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3b47bf5. Reordered serve_grpc so _warmup() + servicer.start_generation_loop() run before await server.start(). The window where the servicer accepts RPCs but has no gen thread dispatching is eliminated. Good catch — HealthCheck always returning OK meant the router couldn't detect this state on its own.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a05ac9cca5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +465 to +466
self._request_uid_map.pop(request_id, None)
self._uid_queues.pop(uid, None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve abort mapping until backend request is removed

When Generate exits through cancellation (for example client disconnects or deadline expiry), the finally block immediately removes request_id -> uid and queue entries but never removes the UID from BatchGenerator. If the follow-up Abort RPC arrives after this cleanup, Abort cannot find the UID and skips batch_generator.remove, so the cancelled request keeps generating tokens until its own stop/max-tokens condition. This wastes decode capacity and can throttle other requests under load.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3b47bf5. Generate's finally block now calls batch_generator.remove([uid]) under _gen_lock, with a swallow-on-exception guard since the gen thread's finish-path or Abort may have already removed it. Client disconnects / deadline cancellations now reclaim the batch slot immediately instead of letting the decode run to natural completion.

Four lazy imports that were inside function bodies are now module-level:
- server.py: mlx_lm.load, huggingface_hub.snapshot_download,
  mlx_lm.generate.BatchGenerator
- servicer.py: smg_grpc_proto.generated.common_pb2

All of these are hard dependencies of the module path (mlx_lm is
installed via the [mlx] extra; common_pb2 is in smg_grpc_proto which
is already imported at top-level), so the lazy form was redundant
protection. Matches the convention already used for mlx.core and
mlx_lm.generate.generation_stream.

Signed-off-by: key4ng <rukeyang@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d3638ed32

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread grpc_servicer/pyproject.toml Outdated
[project.optional-dependencies]
vllm = ["vllm>=0.19.0"]
sglang = ["sglang>=0.5.10"]
mlx = ["mlx>=0.22.0", "mlx-lm>=0.22.0"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bump proto dependency floor for MLX runtime

Adding the mlx extra here exposes the MLX servicer path, but smg-grpc-servicer still allows any smg-grpc-proto>=0.4.6; that range does not guarantee mlx_engine_pb2 is present, while smg_grpc_servicer.mlx.server now imports it unconditionally. In environments that resolve to a pre-MLX proto build that still satisfies this spec, the MLX server fails at import/startup time. Please raise the minimum smg-grpc-proto version to the first release that includes MLX stubs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3b47bf5. Bumped crates/grpc_client/python/pyproject.toml to 0.4.7 (first version that ships mlx_engine_pb2 — the main-branch 0.4.6 does not) and pinned [mlx] extra in grpc_servicer/pyproject.toml to require smg-grpc-proto>=0.4.7. Installing smg-grpc-servicer[mlx] against an older proto build now fails at dependency resolution instead of at runtime import.

Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py
1. server.py: reorder startup. Warmup + start_generation_loop now run
   BEFORE await server.start(), so an incoming Generate RPC can never
   land in the window where the servicer is listening but no gen thread
   is dispatching (which would hang the RPC forever since HealthCheck
   always returns OK and can't signal this state).

2. servicer.py Generate finally: call batch_generator.remove(uid) on
   any Generate exit (client disconnect, deadline, CancelledError).
   Previously only Abort and natural finish removed from the batch, so
   a cancelled Generate would keep decoding until its own stop/max
   condition, wasting batch slots.

3. pyproject.toml: bump smg-grpc-proto to 0.4.7 (first version that
   ships mlx_engine_pb2) and pin [mlx] extra to require >=0.4.7 so
   installing against an older proto build can't crash at import.

4. servicer.py Abort: drain already-buffered tokens from the
   per-request queue before putting the None sentinel. Without this,
   a slow consumer with a backlog would keep receiving stale tokens
   after abort.

Signed-off-by: key4ng <rukeyang@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b47bf5558

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread grpc_servicer/smg_grpc_servicer/mlx/servicer.py
HealthCheck previously hardcoded healthy=True, so the router kept
routing traffic to an MLX worker even if:
  - the generation thread crashed (uncaught exception in _gen_loop)
  - the servicer is mid-shutdown
  - start_generation_loop() hasn't run yet (startup window)

Now derive healthy from three cheap state checks:
  - _shutdown_event.is_set() → shutting down
  - _gen_thread is None → not initialized
  - not _gen_thread.is_alive() → thread died

Message field carries the specific reason for operator debugging.

Signed-off-by: key4ng <rukeyang@gmail.com>
@key4ng

key4ng commented Apr 24, 2026

Copy link
Copy Markdown
Member Author

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5c24ce2b92

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# smg-grpc-proto>=0.4.7 is the first release that ships mlx_engine_pb2;
# without this floor, installing [mlx] against an older proto build would
# crash at import time when smg_grpc_servicer.mlx.server runs.
mlx = ["smg-grpc-proto>=0.4.7", "mlx>=0.22.0", "mlx-lm>=0.22.0"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Raise mlx-lm minimum version to BatchGenerator-capable release

mlx extra currently allows mlx-lm>=0.22.0, but this servicer imports BatchGenerator, SequenceStateMachine, and generation_stream from mlx_lm.generate (mlx/server.py and mlx/servicer.py). In mlx-lm 0.22.x those symbols are not available, so a resolver that pins 0.22.* (still valid under this constraint) will fail at import/startup time before serving any RPCs. Please bump the mlx-lm floor to the first version that exports this batching/state-machine API.

Useful? React with 👍 / 👎.

@slin1237
slin1237 merged commit e4d4452 into main Apr 27, 2026
74 of 76 checks passed
@slin1237
slin1237 deleted the keyang/mlx-python branch April 27, 2026 00:09
key4ng added a commit that referenced this pull request Apr 27, 2026
Adds the full E2E test surface for the MLX gRPC servicer that landed
in #1099. Split off from #1099 to keep that review focused on the
servicer itself.

What's new
----------
- e2e_test/chat_completions/test_mlx_backend.py — 5 tests covering
  basic chat, streaming, tool calling (Qwen3 native <tool_call>),
  reasoning_content (Qwen3 thinking mode), and max_tokens finish
  reason. Module skips on non-Apple-Silicon hosts.
- .github/workflows/pr-test-mlx.yml — runs the test suite on
  macos-latest (Apple Silicon) with HF model + Cargo target caching.
  Triggers only on MLX-related path changes.

Plumbed through existing infra
------------------------------
- e2e_test/infra/constants.py: Runtime.MLX, is_mlx() helper,
  RUNTIME_LABELS entry, MLX added to LOCAL_RUNTIMES.
- e2e_test/infra/__init__.py: re-exports is_mlx.
- e2e_test/infra/model_specs.py: mlx-community/Qwen3-0.6B-4bit
  spec (~400 MB; smallest model that has both tool calling and
  thinking mode for combined coverage).
- e2e_test/infra/worker.py: _build_mlx_cmd() — gRPC-only
  (rejects HTTP since the MLX servicer doesn't expose one).

Test plan
---------
Verified locally on Apple Silicon: 5/5 pass against the MLX servicer
already on main.

Signed-off-by: key4ng <rukeyang@gmail.com>
key4ng added a commit that referenced this pull request May 8, 2026
Adds the full E2E test surface for the MLX gRPC servicer that landed
in #1099. Split off from #1099 to keep that review focused on the
servicer itself.

What's new
----------
- e2e_test/chat_completions/test_mlx_backend.py — 5 tests covering
  basic chat, streaming, tool calling (Qwen3 native <tool_call>),
  reasoning_content (Qwen3 thinking mode), and max_tokens finish
  reason. Module skips on non-Apple-Silicon hosts.
- .github/workflows/pr-test-mlx.yml — runs the test suite on
  macos-latest (Apple Silicon) with HF model + Cargo target caching.
  Triggers only on MLX-related path changes.

Plumbed through existing infra
------------------------------
- e2e_test/infra/constants.py: Runtime.MLX, is_mlx() helper,
  RUNTIME_LABELS entry, MLX added to LOCAL_RUNTIMES.
- e2e_test/infra/__init__.py: re-exports is_mlx.
- e2e_test/infra/model_specs.py: mlx-community/Qwen3-0.6B-4bit
  spec (~400 MB; smallest model that has both tool calling and
  thinking mode for combined coverage).
- e2e_test/infra/worker.py: _build_mlx_cmd() — gRPC-only
  (rejects HTTP since the MLX servicer doesn't expose one).

Test plan
---------
Verified locally on Apple Silicon: 5/5 pass against the MLX servicer
already on main.

Signed-off-by: key4ng <rukeyang@gmail.com>
zach-li-sudo added a commit to zach-li-sudo/smg that referenced this pull request May 10, 2026
key4ng added a commit that referenced this pull request May 14, 2026
Adds the full E2E test surface for the MLX gRPC servicer that landed
in #1099. Split off from #1099 to keep that review focused on the
servicer itself.

What's new
----------
- e2e_test/chat_completions/test_mlx_backend.py — 5 tests covering
  basic chat, streaming, tool calling (Qwen3 native <tool_call>),
  reasoning_content (Qwen3 thinking mode), and max_tokens finish
  reason. Module skips on non-Apple-Silicon hosts.
- .github/workflows/pr-test-mlx.yml — runs the test suite on
  macos-latest (Apple Silicon) with HF model + Cargo target caching.
  Triggers only on MLX-related path changes.

Plumbed through existing infra
------------------------------
- e2e_test/infra/constants.py: Runtime.MLX, is_mlx() helper,
  RUNTIME_LABELS entry, MLX added to LOCAL_RUNTIMES.
- e2e_test/infra/__init__.py: re-exports is_mlx.
- e2e_test/infra/model_specs.py: mlx-community/Qwen3-0.6B-4bit
  spec (~400 MB; smallest model that has both tool calling and
  thinking mode for combined coverage).
- e2e_test/infra/worker.py: _build_mlx_cmd() — gRPC-only
  (rejects HTTP since the MLX servicer doesn't expose one).

Test plan
---------
Verified locally on Apple Silicon: 5/5 pass against the MLX servicer
already on main.

Signed-off-by: key4ng <rukeyang@gmail.com>
zach-li-sudo added a commit to zach-li-sudo/smg that referenced this pull request May 19, 2026
zach-li-sudo added a commit to zach-li-sudo/smg that referenced this pull request May 22, 2026
zach-li-sudo added a commit to zach-li-sudo/smg that referenced this pull request May 23, 2026
zach-li-sudo added a commit to zach-li-sudo/smg that referenced this pull request May 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Dependency updates grpc gRPC client and router changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants