unigilby commited on
Commit
ad64a4b
·
verified ·
1 Parent(s): 4a064a4

Update patches to match upstream PR jundot/omlx#1881

Browse files
patches/README.md CHANGED
@@ -1,27 +1,28 @@
1
  # oMLX patches for serving MiniMax-M3-oQ4
2
 
3
- These are the oMLX-side changes needed to **serve** this checkpoint (beyond mlx-vlm PR #1374). They
4
- are **not yet in oMLX main** — they apply cleanly onto current main (verified). A PR is being opened
5
- upstream; until merged, apply them to your oMLX install.
 
6
 
7
- Apply from the oMLX package root (the dir containing `scheduler.py`, `server.py`, `oq.py`, `api/`):
8
 
9
  ```bash
10
- patch -p0 oq.py < patches/oq.py.patch
11
- patch -p0 scheduler.py < patches/scheduler.py.patch
12
- patch -p0 api/utils.py < patches/api_utils.py.patch
13
  patch -p0 api/tool_calling.py < patches/api_tool_calling.py.patch
14
- patch -p0 server.py < patches/server.py.patch
15
  ```
16
 
17
  | Patch | Purpose | Needed for |
18
  |-------|---------|-----------|
19
  | `scheduler.py.patch` | Serialize `minimax_m3_vl` (like Llama-4) + MiniMax-Sparse-Attention KV cache (`MiniMaxM3KVCache` ↔ batch variant) | running under oMLX batching / concurrency |
20
- | `api_utils.py.patch` | Map `<mm:think>`/`</mm:think>` → `<think>`/`</think>` | clean `reasoning_content` |
21
  | `api_tool_calling.py.patch` + `server.py.patch` | Parse `<invoke name=…>` + bare `<key>value</key>` params; strip the `]<]minimax[>[` token (200058) from streamed content | structured `tool_calls`, no markup leak |
22
  | `oq.py.patch` | `_vlm_sanitize` proxy so oQ can quantize a VLM without instantiating it | **quantizing only** — not needed to serve this checkpoint |
23
 
24
  Notes:
25
- - The tool-call parser belongs more naturally in mlx-vlm's `tool_parsers` (then selectable via
26
- `tokenizer_config.json` `tool_parser_type` with no oMLX patch); a separate mlx-vlm PR is the long-term home.
27
  - `trust_remote_code: true` and `torch`+`torchvision` in the serving env are also required (see the main README).
 
 
 
1
  # oMLX patches for serving MiniMax-M3-oQ4
2
 
3
+ These are the oMLX-side changes needed to **serve** this checkpoint (beyond mlx-vlm PR
4
+ [#1374](https://github.com/Blaizzy/mlx-vlm/pull/1374)). They are **submitted upstream as
5
+ [jundot/omlx#1881](https://github.com/jundot/omlx/pull/1881)** once that merges, no patches are
6
+ needed. Until then, they apply cleanly onto oMLX `main`.
7
 
8
+ Apply from the `omlx/` package root (the dir containing `scheduler.py`, `server.py`, `oq.py`, `api/`):
9
 
10
  ```bash
11
+ patch -p0 oq.py < patches/oq.py.patch
12
+ patch -p0 scheduler.py < patches/scheduler.py.patch
13
+ patch -p0 api/thinking.py < patches/api_thinking.py.patch
14
  patch -p0 api/tool_calling.py < patches/api_tool_calling.py.patch
15
+ patch -p0 server.py < patches/server.py.patch
16
  ```
17
 
18
  | Patch | Purpose | Needed for |
19
  |-------|---------|-----------|
20
  | `scheduler.py.patch` | Serialize `minimax_m3_vl` (like Llama-4) + MiniMax-Sparse-Attention KV cache (`MiniMaxM3KVCache` ↔ batch variant) | running under oMLX batching / concurrency |
21
+ | `api_thinking.py.patch` | Normalize `<mm:think>`/`</mm:think>` → `<think>`/`</think>` in `extract_thinking` + `ThinkingParser.feed` | clean `reasoning_content` |
22
  | `api_tool_calling.py.patch` + `server.py.patch` | Parse `<invoke name=…>` + bare `<key>value</key>` params; strip the `]<]minimax[>[` token (200058) from streamed content | structured `tool_calls`, no markup leak |
23
  | `oq.py.patch` | `_vlm_sanitize` proxy so oQ can quantize a VLM without instantiating it | **quantizing only** — not needed to serve this checkpoint |
24
 
25
  Notes:
 
 
26
  - `trust_remote_code: true` and `torch`+`torchvision` in the serving env are also required (see the main README).
27
+ - The tool-call parser could instead live in mlx-vlm's `tool_parsers` (selectable via `tokenizer_config.json`
28
+ `tool_parser_type`); see the discussion on #1881.
patches/api_thinking.py.patch ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/api/thinking.py b/api/thinking.py
2
+ index 024c0df..e245c14 100644
3
+ --- a/api/thinking.py
4
+ +++ b/api/thinking.py
5
+ @@ -168,6 +168,12 @@ def extract_thinking(text: str) -> Tuple[str, str]:
6
+ if not text:
7
+ return ("", "")
8
+
9
+ + # MiniMax-M3 (minimax_m3_vl) emits reasoning wrapped in <mm:think>...</mm:think>;
10
+ + # normalize to the standard <think>/</think> so it separates into reasoning_content.
11
+ + # No-op when absent.
12
+ + if "<mm:think>" in text or "</mm:think>" in text:
13
+ + text = text.replace("<mm:think>", "<think>").replace("</mm:think>", "</think>")
14
+ +
15
+ thinking_parts = []
16
+ remaining = text
17
+
18
+ @@ -249,6 +255,11 @@ class ThinkingParser:
19
+ if not text:
20
+ return ("", "")
21
+
22
+ + # MiniMax-M3 emits <mm:think>...</mm:think> (each marker is a single token,
23
+ + # so it arrives whole per chunk); normalize to <think>/</think>.
24
+ + if "<mm:think>" in text or "</mm:think>" in text:
25
+ + text = text.replace("<mm:think>", "<think>").replace("</mm:think>", "</think>")
26
+ +
27
+ # Prepend any buffered partial tag content
28
+ text = self._buffer + text
29
+ self._buffer = ""
patches/api_tool_calling.py.patch CHANGED
@@ -1,11 +1,11 @@
1
- --- api/tool_calling.py.bak.minimax-m3-toolcall 2026-06-14 21:39:55
2
- +++ api/tool_calling.py 2026-06-14 21:43:27
3
- @@ -464,6 +464,56 @@
4
- results.append({"name": func_name, "arguments": arguments})
5
-
6
  return results[0] if len(results) == 1 else results
7
- +
8
- +
9
  +def _parse_minimax_m3_tool_calls(text):
10
  + """Parse MiniMax-M3 (minimax_m3_vl) tool calls.
11
  +
@@ -54,10 +54,12 @@
54
  + cleaned = re.sub(r"<invoke\s+name=.*?</invoke>", "", cleaned, flags=re.DOTALL)
55
  + cleaned = cleaned.replace(NS, "").strip()
56
  + return cleaned, tool_calls
57
-
58
-
59
  def parse_tool_calls(
60
- @@ -623,6 +673,12 @@
 
 
61
  cleaned_text = cleaned_text[:idx].strip()
62
  return cleaned_text, tool_calls
63
 
 
1
+ diff --git a/api/tool_calling.py b/api/tool_calling.py
2
+ index b51bcbb..9f3f425 100644
3
+ --- a/api/tool_calling.py
4
+ +++ b/api/tool_calling.py
5
+ @@ -509,6 +509,56 @@ def _parse_gemma4_tool_call_fallback(text: str) -> Union[dict, list]:
6
  return results[0] if len(results) == 1 else results
7
+
8
+
9
  +def _parse_minimax_m3_tool_calls(text):
10
  + """Parse MiniMax-M3 (minimax_m3_vl) tool calls.
11
  +
 
54
  + cleaned = re.sub(r"<invoke\s+name=.*?</invoke>", "", cleaned, flags=re.DOTALL)
55
  + cleaned = cleaned.replace(NS, "").strip()
56
  + return cleaned, tool_calls
57
+ +
58
+ +
59
  def parse_tool_calls(
60
+ text: str,
61
+ tokenizer: Any,
62
+ @@ -666,6 +716,12 @@ def parse_tool_calls(
63
  cleaned_text = cleaned_text[:idx].strip()
64
  return cleaned_text, tool_calls
65
 
patches/oq.py.patch CHANGED
@@ -1,6 +1,8 @@
1
- --- oq.py.bak.minimax-m3.20260613 2026-06-13 11:41:21
2
- +++ oq.py 2026-06-13 11:41:21
3
- @@ -2030,6 +2030,13 @@
 
 
4
 
5
  proxy = _Proxy()
6
  proxy.config = model_config
 
1
+ diff --git a/oq.py b/oq.py
2
+ index 295d9be..06a8268 100644
3
+ --- a/oq.py
4
+ +++ b/oq.py
5
+ @@ -1993,6 +1993,13 @@ def _build_model_sanitizer(config: dict, text_only: bool = False):
6
 
7
  proxy = _Proxy()
8
  proxy.config = model_config
patches/scheduler.py.patch CHANGED
@@ -1,8 +1,10 @@
1
- --- scheduler.py.bak.minimax-m3.20260613 2026-06-13 12:41:33
2
- +++ scheduler.py 2026-06-13 13:15:21
3
- @@ -577,6 +577,12 @@
4
- return type(cache_obj)(*converted)
5
- if isinstance(cache_obj, _REGULAR_SINGLETON_CACHE_TYPES):
 
 
6
  return cache_obj.merge([cache_obj])
7
  + # MiniMax-M3: convert its single MSA cache to the batch variant
8
  + # (MiniMaxM3BatchKVCache implements filter/extend/extract/merge). Without
@@ -13,13 +15,10 @@
13
  return cache_obj
14
 
15
 
16
- @@ -755,6 +761,54 @@
17
- "ChunkedKVCache patch: methods already present upstream, " "skipped: %s",
18
- ", ".join(_ckvcache_methods_skipped),
19
- )
20
- +except ImportError:
21
- + pass
22
- +
23
  +# MiniMax-M3 (minimax_m3_vl): its MSA KV cache lacks the batch-aware methods
24
  +# (filter/extract/merge/extend) oMLX continuous-batching expects, so serving
25
  +# raises "MiniMaxM3KVCache does not yet support batching with history". M3
@@ -65,10 +64,13 @@
65
  + "supported; serialize MiniMax-M3 requests."
66
  + )
67
  + _M3KV.extend = _m3_extend_passthrough
68
- except ImportError:
69
- pass
 
70
 
71
- @@ -961,7 +1015,7 @@
 
 
72
  continue
73
  seen.add(obj_id)
74
 
 
1
+ diff --git a/scheduler.py b/scheduler.py
2
+ index 2192471..3cb9b12 100644
3
+ --- a/scheduler.py
4
+ +++ b/scheduler.py
5
+ @@ -806,6 +806,12 @@ def _to_batched_cache_layer(cache_obj: Any) -> Any:
6
+ and type(cache_obj) is _TQ_SINGLETON_CACHE_TYPE
7
+ ):
8
  return cache_obj.merge([cache_obj])
9
  + # MiniMax-M3: convert its single MSA cache to the batch variant
10
  + # (MiniMaxM3BatchKVCache implements filter/extend/extract/merge). Without
 
15
  return cache_obj
16
 
17
 
18
+ @@ -987,6 +993,54 @@ try:
19
+ except ImportError:
20
+ pass
21
+
 
 
 
22
  +# MiniMax-M3 (minimax_m3_vl): its MSA KV cache lacks the batch-aware methods
23
  +# (filter/extract/merge/extend) oMLX continuous-batching expects, so serving
24
  +# raises "MiniMaxM3KVCache does not yet support batching with history". M3
 
64
  + "supported; serialize MiniMax-M3 requests."
65
  + )
66
  + _M3KV.extend = _m3_extend_passthrough
67
+ +except ImportError:
68
+ + pass
69
+ +
70
 
71
+ # ---------------------------------------------------------------------------
72
+ # Monkey-patch PromptProcessingBatch.prompt to set mRoPE deltas before the
73
+ @@ -1207,7 +1261,7 @@ def _model_declares_llama4(model: Any) -> bool:
74
  continue
75
  seen.add(obj_id)
76
 
patches/server.py.patch CHANGED
@@ -1,6 +1,8 @@
1
- --- server.py.bak.minimax-m3-ns 2026-06-14 21:39:55
2
- +++ server.py 2026-06-14 21:43:45
3
- @@ -3969,7 +3969,7 @@
 
 
4
  # tools are present so clients see clean streamed text.
5
  if content_delta:
6
  if tool_filter:
@@ -9,7 +11,7 @@
9
  if content_delta:
10
  chunk = ChatCompletionChunk(
11
  id=response_id,
12
- @@ -4031,7 +4031,7 @@
13
  yield f"data: {chunk.model_dump_json(exclude_none=True)}\n\n"
14
  if content_delta:
15
  if tool_filter:
@@ -18,7 +20,7 @@
18
  if content_delta:
19
  chunk = ChatCompletionChunk(
20
  id=response_id,
21
- @@ -4046,7 +4046,7 @@
22
  yield f"data: {chunk.model_dump_json(exclude_none=True)}\n\n"
23
 
24
  if tool_filter:
@@ -27,7 +29,7 @@
27
  if remaining:
28
  chunk = ChatCompletionChunk(
29
  id=response_id,
30
- @@ -4390,7 +4390,7 @@
31
  # markup when a known start marker is available.
32
  if content_delta:
33
  if tool_filter:
@@ -36,7 +38,7 @@
36
  if content_delta:
37
  # When tools are requested AND we haven't yet opened
38
  # a text block, drop pure-whitespace deltas. Models
39
- @@ -4467,7 +4467,7 @@
40
  )
41
  if content_delta:
42
  if tool_filter:
@@ -45,7 +47,7 @@
45
  if content_delta:
46
  if thinking_block_started and not text_block_started:
47
  yield create_content_block_stop_event(index=block_index)
48
- @@ -4482,7 +4482,7 @@
49
 
50
  # Flush any remaining buffered content from the tool-call filter
51
  if tool_filter:
@@ -54,7 +56,7 @@
54
  if remaining:
55
  if not text_block_started:
56
  if thinking_block_started:
57
- @@ -5736,7 +5736,7 @@
58
  if msg_output_index is None:
59
  msg_output_index = next_output_index
60
  if tool_filter:
@@ -63,7 +65,7 @@
63
  if content_delta:
64
  seq += 1
65
  yield format_sse_event(
66
- @@ -5783,7 +5783,7 @@
67
  accumulated_reasoning += thinking_delta
68
  if content_delta:
69
  if tool_filter:
@@ -72,7 +74,7 @@
72
  if content_delta:
73
  seq += 1
74
  yield format_sse_event(
75
- @@ -5798,7 +5798,7 @@
76
  },
77
  )
78
  if tool_filter:
 
1
+ diff --git a/server.py b/server.py
2
+ index f58b000..88705e2 100644
3
+ --- a/server.py
4
+ +++ b/server.py
5
+ @@ -4002,7 +4002,7 @@ async def stream_chat_completion(
6
  # tools are present so clients see clean streamed text.
7
  if content_delta:
8
  if tool_filter:
 
11
  if content_delta:
12
  chunk = ChatCompletionChunk(
13
  id=response_id,
14
+ @@ -4062,7 +4062,7 @@ async def stream_chat_completion(
15
  yield f"data: {chunk.model_dump_json(exclude_none=True)}\n\n"
16
  if content_delta:
17
  if tool_filter:
 
20
  if content_delta:
21
  chunk = ChatCompletionChunk(
22
  id=response_id,
23
+ @@ -4077,7 +4077,7 @@ async def stream_chat_completion(
24
  yield f"data: {chunk.model_dump_json(exclude_none=True)}\n\n"
25
 
26
  if tool_filter:
 
29
  if remaining:
30
  chunk = ChatCompletionChunk(
31
  id=response_id,
32
+ @@ -4419,7 +4419,7 @@ async def stream_anthropic_messages(
33
  # markup when a known start marker is available.
34
  if content_delta:
35
  if tool_filter:
 
38
  if content_delta:
39
  # When tools are requested AND we haven't yet opened
40
  # a text block, drop pure-whitespace deltas. Models
41
+ @@ -4494,7 +4494,7 @@ async def stream_anthropic_messages(
42
  )
43
  if content_delta:
44
  if tool_filter:
 
47
  if content_delta:
48
  if thinking_block_started and not text_block_started:
49
  yield create_content_block_stop_event(index=block_index)
50
+ @@ -4509,7 +4509,7 @@ async def stream_anthropic_messages(
51
 
52
  # Flush any remaining buffered content from the tool-call filter
53
  if tool_filter:
 
56
  if remaining:
57
  if not text_block_started:
58
  if thinking_block_started:
59
+ @@ -5774,7 +5774,7 @@ async def stream_responses_api(
60
  if msg_output_index is None:
61
  msg_output_index = next_output_index
62
  if tool_filter:
 
65
  if content_delta:
66
  seq += 1
67
  yield format_sse_event(
68
+ @@ -5819,7 +5819,7 @@ async def stream_responses_api(
69
  accumulated_reasoning += thinking_delta
70
  if content_delta:
71
  if tool_filter:
 
74
  if content_delta:
75
  seq += 1
76
  yield format_sse_event(
77
+ @@ -5834,7 +5834,7 @@ async def stream_responses_api(
78
  },
79
  )
80
  if tool_filter: