mirror of
https://github.com/exo-explore/exo.git
synced 2026-09-08 19:41:32 -04:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bc74d2cea | ||
|
|
5ece607264 | ||
|
|
8106d8a7e3 | ||
|
|
2ef3a4c707 | ||
|
|
bba012f15b | ||
|
|
3babf9d070 | ||
|
|
5d7ea4c6c0 | ||
|
|
e116097f64 | ||
|
|
c6467094b1 |
No files matched your search
@@ -383,11 +383,12 @@ class GenerationBatch:
|
||||
state_machines: List[SequenceStateMachine]
|
||||
max_tokens: List[int]
|
||||
_current_tokens: Optional[mx.array]
|
||||
_current_logprobs: List[mx.array]
|
||||
_next_tokens: mx.array
|
||||
_next_logprobs: List[mx.array]
|
||||
_token_context: List[mx.array]
|
||||
_current_logprobs: mx.array | List[mx.array]
|
||||
_next_tokens: Optional[mx.array]
|
||||
_next_logprobs: mx.array | List[mx.array]
|
||||
_token_context: List[Any]
|
||||
_num_tokens: List[int]
|
||||
_matcher_states: List[Any]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
"""Type stubs for mlx_lm.models.deepseek_v4"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import mlx.core as mx
|
||||
import mlx.nn as nn
|
||||
|
||||
from .base import BaseModelArgs
|
||||
from .cache import ArraysCache, RotatingKVCache
|
||||
from .switch_layers import SwitchGLU
|
||||
|
||||
@dataclass
|
||||
class ModelArgs(BaseModelArgs):
|
||||
model_type: str
|
||||
vocab_size: int
|
||||
hidden_size: int
|
||||
intermediate_size: int
|
||||
moe_intermediate_size: int
|
||||
num_hidden_layers: int
|
||||
num_attention_heads: int
|
||||
num_key_value_heads: int
|
||||
n_shared_experts: Optional[int]
|
||||
n_routed_experts: int
|
||||
num_experts_per_tok: int
|
||||
head_dim: int
|
||||
qk_rope_head_dim: int
|
||||
q_lora_rank: int
|
||||
o_lora_rank: int
|
||||
o_groups: int
|
||||
sliding_window: int
|
||||
hc_mult: int
|
||||
hc_sinkhorn_iters: int
|
||||
hc_eps: float
|
||||
compress_ratios: Optional[List[int]]
|
||||
compress_rope_theta: float
|
||||
rope_theta: float
|
||||
rope_scaling: Optional[Dict[str, Any]]
|
||||
rms_norm_eps: float
|
||||
swiglu_limit: float
|
||||
attention_bias: bool
|
||||
max_position_embeddings: int
|
||||
|
||||
class DeepseekV4RoPE(nn.Module):
|
||||
dims: int
|
||||
freqs: mx.array
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dims: int,
|
||||
base: float,
|
||||
scaling_config: Optional[Dict[str, Any]] = None,
|
||||
) -> None: ...
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
offset: int = 0,
|
||||
inverse: bool = False,
|
||||
) -> mx.array: ...
|
||||
|
||||
class HyperConnection(nn.Module):
|
||||
dim: int
|
||||
hc_mult: int
|
||||
norm_eps: float
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
hc_mult: int,
|
||||
norm_eps: float,
|
||||
sinkhorn_iters: int,
|
||||
hc_eps: float,
|
||||
) -> None: ...
|
||||
|
||||
class HyperHead(nn.Module):
|
||||
dim: int
|
||||
hc_mult: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
hc_mult: int,
|
||||
norm_eps: float,
|
||||
hc_eps: float,
|
||||
) -> None: ...
|
||||
def __call__(self, x: mx.array) -> mx.array: ...
|
||||
|
||||
class Compressor(nn.Module):
|
||||
dim: int
|
||||
head_dim: int
|
||||
rope_head_dim: int
|
||||
compress_ratio: int
|
||||
overlap: bool
|
||||
wkv_gate: nn.Linear
|
||||
ape: mx.array
|
||||
norm: nn.RMSNorm
|
||||
rope: DeepseekV4RoPE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
compress_ratio: int,
|
||||
head_dim: int,
|
||||
rope_head_dim: int,
|
||||
rms_norm_eps: float,
|
||||
rope: DeepseekV4RoPE,
|
||||
) -> None: ...
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
state: ArraysCache,
|
||||
offset: Any,
|
||||
slot_compressed: int,
|
||||
slot_kv_state: int,
|
||||
slot_score_state: int,
|
||||
) -> Optional[mx.array]: ...
|
||||
|
||||
class Indexer(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
args: ModelArgs,
|
||||
compress_ratio: int,
|
||||
rope: DeepseekV4RoPE,
|
||||
) -> None: ...
|
||||
|
||||
class DeepseekV4Cache:
|
||||
local: Any
|
||||
offset: int
|
||||
keys: Optional[mx.array]
|
||||
values: Optional[mx.array]
|
||||
state: Any
|
||||
meta_state: Any
|
||||
nbytes: int
|
||||
|
||||
def __init__(self, sliding_window: int) -> None: ...
|
||||
def update_and_fetch(
|
||||
self, keys: mx.array, values: mx.array
|
||||
) -> tuple[mx.array, mx.array]: ...
|
||||
def is_trimmable(self) -> bool: ...
|
||||
def trim(self, n: int) -> int: ...
|
||||
def empty(self) -> bool: ...
|
||||
def size(self) -> int: ...
|
||||
def prepare(
|
||||
self,
|
||||
*,
|
||||
left_padding: Optional[List[int]] = None,
|
||||
lengths: Optional[List[int]] = None,
|
||||
right_padding: Optional[List[int]] = None,
|
||||
) -> None: ...
|
||||
def finalize(self) -> None: ...
|
||||
def filter(self, batch_indices: mx.array) -> None: ...
|
||||
def extend(self, other: "DeepseekV4Cache") -> None: ...
|
||||
def extract(self, idx: int) -> "DeepseekV4Cache": ...
|
||||
@classmethod
|
||||
def merge(cls, caches: List["DeepseekV4Cache"]) -> "DeepseekV4Cache": ...
|
||||
|
||||
class V4Attention(nn.Module):
|
||||
args: ModelArgs
|
||||
layer_id: int
|
||||
dim: int
|
||||
n_heads: int
|
||||
head_dim: int
|
||||
rope_head_dim: int
|
||||
nope_head_dim: int
|
||||
n_groups: int
|
||||
q_lora_rank: int
|
||||
o_lora_rank: int
|
||||
window: int
|
||||
eps: float
|
||||
scale: float
|
||||
compress_ratio: int
|
||||
wqkv_a: nn.Linear
|
||||
q_norm: nn.RMSNorm
|
||||
wq_b: nn.Linear
|
||||
kv_norm: nn.RMSNorm
|
||||
attn_sink: mx.array
|
||||
wo_a: nn.Linear
|
||||
wo_b: nn.Linear
|
||||
rope: DeepseekV4RoPE
|
||||
compressor: Compressor
|
||||
indexer: Indexer
|
||||
|
||||
def __init__(self, args: ModelArgs, layer_id: int) -> None: ...
|
||||
def __call__(
|
||||
self,
|
||||
x: mx.array,
|
||||
mask: Optional[mx.array] = None,
|
||||
cache: Optional[Any] = None,
|
||||
) -> mx.array: ...
|
||||
|
||||
class DeepseekV4MLP(nn.Module):
|
||||
gate_proj: nn.Linear
|
||||
up_proj: nn.Linear
|
||||
down_proj: nn.Linear
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hidden_size: int,
|
||||
intermediate_size: int,
|
||||
swiglu_limit: float = 0.0,
|
||||
) -> None: ...
|
||||
def __call__(self, x: mx.array) -> mx.array: ...
|
||||
|
||||
class MoEGate(nn.Module):
|
||||
weight: mx.array
|
||||
|
||||
def __init__(self, args: ModelArgs, layer_id: int) -> None: ...
|
||||
def __call__(
|
||||
self, x: mx.array, input_ids: mx.array
|
||||
) -> tuple[mx.array, mx.array]: ...
|
||||
|
||||
class DeepseekV4MoE(nn.Module):
|
||||
num_experts_per_tok: int
|
||||
switch_mlp: SwitchGLU
|
||||
gate: MoEGate
|
||||
shared_experts: DeepseekV4MLP
|
||||
|
||||
def __init__(self, args: ModelArgs, layer_id: int) -> None: ...
|
||||
def __call__(self, x: mx.array, input_ids: mx.array) -> mx.array: ...
|
||||
|
||||
class DeepseekV4Block(nn.Module):
|
||||
attn_norm: nn.RMSNorm
|
||||
attn: V4Attention
|
||||
hc_attn: HyperConnection
|
||||
ffn_norm: nn.RMSNorm
|
||||
ffn: DeepseekV4MoE
|
||||
hc_ffn: HyperConnection
|
||||
|
||||
def __init__(self, args: ModelArgs, layer_id: int) -> None: ...
|
||||
def __call__(
|
||||
self,
|
||||
h: mx.array,
|
||||
cache: Optional[Any],
|
||||
input_ids: mx.array,
|
||||
) -> mx.array: ...
|
||||
|
||||
class DeepseekV4Model(nn.Module):
|
||||
args: ModelArgs
|
||||
vocab_size: int
|
||||
embed_tokens: nn.Embedding
|
||||
layers: list[DeepseekV4Block]
|
||||
norm: nn.RMSNorm
|
||||
hc_head: HyperHead
|
||||
|
||||
def __init__(self, args: ModelArgs) -> None: ...
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[List[Any]] = None,
|
||||
) -> mx.array: ...
|
||||
|
||||
class Model(nn.Module):
|
||||
args: ModelArgs
|
||||
model_type: str
|
||||
model: DeepseekV4Model
|
||||
lm_head: nn.Linear
|
||||
|
||||
def __init__(self, args: ModelArgs) -> None: ...
|
||||
def __call__(
|
||||
self,
|
||||
inputs: mx.array,
|
||||
cache: Optional[List[Any]] = None,
|
||||
) -> mx.array: ...
|
||||
def sanitize(self, weights: dict[str, Any]) -> dict[str, Any]: ...
|
||||
def make_cache(self) -> list[RotatingKVCache | DeepseekV4Cache]: ...
|
||||
@property
|
||||
def layers(self) -> list[DeepseekV4Block]: ...
|
||||
+55
-5
@@ -122,8 +122,48 @@ def load_tokenizer_for_bench(model_id: str) -> Any:
|
||||
|
||||
return hf_tokenizer
|
||||
|
||||
# Default: use AutoTokenizer
|
||||
return AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
||||
# TODO: Change back to using only transformers
|
||||
try:
|
||||
return AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
||||
except (AttributeError, ValueError):
|
||||
from huggingface_hub import snapshot_download
|
||||
from transformers import PretrainedConfig
|
||||
|
||||
model_path = Path(
|
||||
snapshot_download(
|
||||
model_id,
|
||||
allow_patterns=[
|
||||
"*.json",
|
||||
"*.py",
|
||||
"tokenizer.model",
|
||||
"*.tiktoken",
|
||||
"tiktoken.model",
|
||||
"*.txt",
|
||||
"*.jsonl",
|
||||
"*.jinja",
|
||||
],
|
||||
)
|
||||
)
|
||||
stub_kwargs: dict[str, Any] = {}
|
||||
config_file = model_path / "config.json"
|
||||
if config_file.exists():
|
||||
with open(config_file) as f:
|
||||
raw = json.load(f)
|
||||
for key in (
|
||||
"model_type",
|
||||
"max_position_embeddings",
|
||||
"vocab_size",
|
||||
"bos_token_id",
|
||||
"eos_token_id",
|
||||
"pad_token_id",
|
||||
):
|
||||
if key in raw:
|
||||
stub_kwargs[key] = raw[key]
|
||||
return AutoTokenizer.from_pretrained(
|
||||
str(model_path),
|
||||
config=PretrainedConfig(**stub_kwargs),
|
||||
trust_remote_code=True,
|
||||
)
|
||||
|
||||
|
||||
def format_peak_memory(b: float) -> str:
|
||||
@@ -278,9 +318,19 @@ class PromptSizer:
|
||||
def _make_counter(tokenizer: Any) -> Callable[[str], int]:
|
||||
def count_fn(user_content: str) -> int:
|
||||
messages = [{"role": "user", "content": user_content}]
|
||||
ids = tokenizer.apply_chat_template(
|
||||
messages, tokenize=True, add_generation_prompt=True
|
||||
)
|
||||
try:
|
||||
ids = tokenizer.apply_chat_template(
|
||||
messages, tokenize=True, add_generation_prompt=True
|
||||
)
|
||||
except ValueError:
|
||||
# Models without a Jinja chat template (e.g. DeepSeek V4 which
|
||||
# ships its own Python encoder). Use the exo-side V4 encoder.
|
||||
from exo.worker.engines.mlx.deepseek_v4_encoding import (
|
||||
encode_messages as encode_v4,
|
||||
)
|
||||
|
||||
prompt = encode_v4(messages, thinking_mode="thinking")
|
||||
ids = tokenizer.encode(prompt, add_special_tokens=False)
|
||||
# Fix for transformers 5.x
|
||||
if hasattr(ids, "input_ids"):
|
||||
ids = ids.input_ids
|
||||
|
||||
+4
-3
@@ -29,11 +29,12 @@ dependencies = [
|
||||
"msgspec>=0.19.0",
|
||||
"zstandard>=0.23.0",
|
||||
"mlx-vlm>=0.3.11",
|
||||
"transformers>=5.0.0,<5.4.0",
|
||||
"transformers>=5.6.2",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
exo = "exo.main:main"
|
||||
exo-reasoning-proxy = "exo.reasoning_proxy.main:main"
|
||||
|
||||
# dependencies only required for development
|
||||
[dependency-groups]
|
||||
@@ -77,7 +78,7 @@ members = ["rust/exo_pyo3_bindings", "bench"]
|
||||
[tool.uv.sources]
|
||||
exo-pyo3-bindings = { workspace = true }
|
||||
mlx = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git", branch = "address-rdma-gpu-locks", marker = "sys_platform == 'darwin'" }
|
||||
mlx-lm = { git = "https://github.com/rltakashige/mlx-lm", branch = "leo/fix-arrayscache-leak" }
|
||||
mlx-lm = { git = "https://github.com/rltakashige/mlx-lm", branch = "leo/deepseek-v4" }
|
||||
torch = [
|
||||
{ index = "pytorch-cu130", marker = "sys_platform == 'linux' and extra == 'cuda13' and extra != 'cpu' and extra != 'cuda12'" },
|
||||
{ index = "pytorch-cu120", marker = "sys_platform == 'linux' and extra == 'cuda12' and extra != 'cpu' and extra != 'cuda13'" },
|
||||
@@ -154,7 +155,7 @@ required-version = ">=0.8.6"
|
||||
prerelease = "allow"
|
||||
environments = ["sys_platform == 'darwin'", "sys_platform == 'linux'"]
|
||||
conflicts = [[{ extra = "cuda12" }, { extra = "cuda13" }, { extra = "cpu" }]]
|
||||
constraint-dependencies = ["transformers>=5.0.0,<5.4.0"]
|
||||
constraint-dependencies = ["transformers>=5.6.2"]
|
||||
override-dependencies = [
|
||||
"mlx==0.31.1; sys_platform=='linux'",
|
||||
"mlx; sys_platform=='darwin'",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
model_id = "mlx-community/DeepSeek-V4-Flash"
|
||||
n_layers = 43
|
||||
hidden_size = 4096
|
||||
num_key_value_heads = 1
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "deepseek"
|
||||
quantization = "8bit"
|
||||
base_model = "DeepSeek V4 Flash"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 1048576
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 155095760030
|
||||
|
||||
# Source: https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash
|
||||
[sampling_defaults]
|
||||
temperature = 1.0
|
||||
top_p = 1.0
|
||||
@@ -0,0 +1,20 @@
|
||||
model_id = "mlx-community/DeepSeek-V4-Pro"
|
||||
n_layers = 61
|
||||
hidden_size = 7168
|
||||
num_key_value_heads = 1
|
||||
supports_tensor = true
|
||||
tasks = ["TextGeneration"]
|
||||
family = "deepseek"
|
||||
quantization = "8bit"
|
||||
base_model = "DeepSeek V4 Pro"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 1048576
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 849681803879
|
||||
|
||||
# Source: https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro
|
||||
[sampling_defaults]
|
||||
temperature = 1.0
|
||||
top_p = 1.0
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "8bit"
|
||||
base_model = "GLM 4.5 Air"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "bf16"
|
||||
base_model = "GLM 4.5 Air"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "4bit"
|
||||
base_model = "GLM 4.7"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "6bit"
|
||||
base_model = "GLM 4.7"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "8bit"
|
||||
base_model = "GLM 4.7"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "4bit"
|
||||
base_model = "GLM 4.7 Flash"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "5bit"
|
||||
base_model = "GLM 4.7 Flash"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "6bit"
|
||||
base_model = "GLM 4.7 Flash"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "8bit"
|
||||
base_model = "GLM 4.7 Flash"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "8bit"
|
||||
base_model = "GLM-5"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "MXFP4-Q8"
|
||||
base_model = "GLM-5"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "bf16"
|
||||
base_model = "GLM-5"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "8bit"
|
||||
base_model = "GLM-5.1"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "MXFP4-Q8"
|
||||
base_model = "GLM-5.1"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "glm"
|
||||
quantization = "bf16"
|
||||
base_model = "GLM-5.1"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "kimi"
|
||||
quantization = ""
|
||||
base_model = "Kimi K2"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
reasoning_dialect = "suffix"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "kimi"
|
||||
quantization = ""
|
||||
base_model = "Kimi K2.5"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "suffix"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "kimi"
|
||||
quantization = "3bit"
|
||||
base_model = "Kimi K2.6"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "suffix"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "minimax"
|
||||
quantization = "3bit"
|
||||
base_model = "MiniMax M2.1"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "minimax"
|
||||
quantization = "8bit"
|
||||
base_model = "MiniMax M2.1"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "minimax"
|
||||
quantization = "4bit"
|
||||
base_model = "MiniMax M2.5"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "minimax"
|
||||
quantization = "6bit"
|
||||
base_model = "MiniMax M2.5"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "minimax"
|
||||
quantization = "8bit"
|
||||
base_model = "MiniMax M2.5"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "minimax"
|
||||
quantization = "4bit-mxfp4"
|
||||
base_model = "MiniMax M2.7"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "minimax"
|
||||
quantization = "4bit"
|
||||
base_model = "MiniMax M2.7"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "minimax"
|
||||
quantization = "5bit"
|
||||
base_model = "MiniMax M2.7"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "minimax"
|
||||
quantization = "6bit"
|
||||
base_model = "MiniMax M2.7"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "minimax"
|
||||
quantization = "8bit"
|
||||
base_model = "MiniMax M2.7"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "minimax"
|
||||
quantization = "bf16"
|
||||
base_model = "MiniMax M2.7"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "4bit"
|
||||
base_model = "Qwen3 Next 80B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "8bit"
|
||||
base_model = "Qwen3 Next 80B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "4bit"
|
||||
base_model = "Qwen3.5 122B A10B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "6bit"
|
||||
base_model = "Qwen3.5 122B A10B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "8bit"
|
||||
base_model = "Qwen3.5 122B A10B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "bf16"
|
||||
base_model = "Qwen3.5 122B A10B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "4bit"
|
||||
base_model = "Qwen3.5 27B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "8bit"
|
||||
base_model = "Qwen3.5 27B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "8bit"
|
||||
base_model = "Qwen3.5 2B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "4bit"
|
||||
base_model = "Qwen3.5 35B A3B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "8bit"
|
||||
base_model = "Qwen3.5 35B A3B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "4bit"
|
||||
base_model = "Qwen3.5 397B A17B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "6bit"
|
||||
base_model = "Qwen3.5 397B A17B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "8bit"
|
||||
base_model = "Qwen3.5 397B A17B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "4bit"
|
||||
base_model = "Qwen3.5 9B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "8bit"
|
||||
base_model = "Qwen3.5 9B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "4bit"
|
||||
base_model = "Qwen3.6 27B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "8bit"
|
||||
base_model = "Qwen3.6 27B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "bf16"
|
||||
base_model = "Qwen3.6 27B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "4bit"
|
||||
base_model = "Qwen3.6 35B A3B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "5bit"
|
||||
base_model = "Qwen3.6 35B A3B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "8bit"
|
||||
base_model = "Qwen3.6 35B A3B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "qwen"
|
||||
quantization = "bf16"
|
||||
base_model = "Qwen3.6 35B A3B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
reasoning_dialect = "post_last_user"
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "gpt-oss"
|
||||
quantization = "MXFP4-Q8"
|
||||
base_model = "GPT-OSS 120B"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "channel"
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -8,7 +8,7 @@ family = "gpt-oss"
|
||||
quantization = "MXFP4-Q8"
|
||||
base_model = "GPT-OSS 20B"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
reasoning_dialect = "channel"
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
|
||||
@@ -1663,6 +1663,7 @@ class API:
|
||||
quantization=card.quantization,
|
||||
base_model=card.base_model,
|
||||
capabilities=card.capabilities,
|
||||
reasoning_dialect=card.reasoning_dialect,
|
||||
context_length=card.context_length,
|
||||
)
|
||||
for card in cards
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field, field_validator
|
||||
from exo.shared.models.model_cards import ModelCard, ModelId
|
||||
from exo.shared.types.common import CommandId, NodeId
|
||||
from exo.shared.types.memory import Memory
|
||||
from exo.shared.types.text_generation import ReasoningEffort
|
||||
from exo.shared.types.text_generation import ReasoningDialect, ReasoningEffort
|
||||
from exo.shared.types.worker.instances import Instance, InstanceId, InstanceMeta
|
||||
from exo.shared.types.worker.shards import Sharding, ShardMetadata
|
||||
from exo.utils.pydantic_ext import FrozenModel
|
||||
@@ -48,6 +48,7 @@ class ModelListModel(BaseModel):
|
||||
quantization: str = Field(default="")
|
||||
base_model: str = Field(default="")
|
||||
capabilities: list[str] = Field(default_factory=list)
|
||||
reasoning_dialect: ReasoningDialect = "none"
|
||||
|
||||
|
||||
class ModelList(BaseModel):
|
||||
|
||||
@@ -133,12 +133,16 @@ def place_instance(
|
||||
f"Requested Tensor sharding but this model does not support tensor parallelism: {command.model_card.model_id}"
|
||||
)
|
||||
# TODO: the condition here for tensor parallel is not correct, but it works good enough for now.
|
||||
# DeepSeek V4 is MQA (num_key_value_heads=1) but its sharding strategy
|
||||
# head-parallelises wq_b/wo_a and shards MoE experts instead of splitting
|
||||
# KV heads, so the kv-head divisibility check doesn't apply.
|
||||
is_deepseek_v4 = command.model_card.base_model.startswith("DeepSeek V4")
|
||||
kv_heads = command.model_card.num_key_value_heads
|
||||
cycles_with_sufficient_memory = [
|
||||
cycle
|
||||
for cycle in cycles_with_sufficient_memory
|
||||
if command.model_card.hidden_size % len(cycle) == 0
|
||||
and (kv_heads is None or kv_heads % len(cycle) == 0)
|
||||
and (is_deepseek_v4 or kv_heads is None or kv_heads % len(cycle) == 0)
|
||||
]
|
||||
if not cycles_with_sufficient_memory:
|
||||
raise ValueError(
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,33 @@
|
||||
from typing import cast
|
||||
|
||||
|
||||
def as_str(value: object) -> str | None:
|
||||
return value if isinstance(value, str) else None
|
||||
|
||||
|
||||
def as_list(value: object) -> list[object] | None:
|
||||
if isinstance(value, list):
|
||||
return cast(list[object], value)
|
||||
return None
|
||||
|
||||
|
||||
def as_dict(value: object) -> dict[str, object] | None:
|
||||
if isinstance(value, dict):
|
||||
return cast(dict[str, object], value)
|
||||
return None
|
||||
|
||||
|
||||
def as_int(value: object, default: int = 0) -> int:
|
||||
return value if isinstance(value, int) and not isinstance(value, bool) else default
|
||||
|
||||
|
||||
def dict_get_str(d: dict[str, object], key: str) -> str | None:
|
||||
return as_str(d.get(key))
|
||||
|
||||
|
||||
def dict_get_list(d: dict[str, object], key: str) -> list[object] | None:
|
||||
return as_list(d.get(key))
|
||||
|
||||
|
||||
def dict_get_dict(d: dict[str, object], key: str) -> dict[str, object] | None:
|
||||
return as_dict(d.get(key))
|
||||
@@ -0,0 +1,261 @@
|
||||
"""Accumulators for capturing the emitted assistant shape from a streaming response.
|
||||
|
||||
Both accumulators are fed raw SSE chunks (bytes) as they pass through. At stream
|
||||
end, they expose a canonical assistant-message shape suitable for hashing, plus
|
||||
the reasoning text that should be cached against that hash.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from exo.reasoning_proxy._helpers import (
|
||||
as_dict,
|
||||
as_str,
|
||||
dict_get_dict,
|
||||
dict_get_list,
|
||||
dict_get_str,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenAIAccumulator:
|
||||
"""Captures content, tool_calls, and reasoning_content from OpenAI SSE chunks.
|
||||
|
||||
OpenAI can emit multiple choices per chunk; we only track choice index 0
|
||||
(the common case for chat completions; n>1 is uncommon and re-hash misses
|
||||
there degrade gracefully to no-op cache insert).
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._content_parts: list[str] = []
|
||||
self._reasoning_parts: list[str] = []
|
||||
self._tool_calls_by_index: dict[int, dict[str, object]] = {}
|
||||
self._buffer = ""
|
||||
|
||||
def feed_bytes(self, chunk: bytes) -> None:
|
||||
self._buffer += chunk.decode("utf-8", errors="replace")
|
||||
while "\n" in self._buffer:
|
||||
line, self._buffer = self._buffer.split("\n", 1)
|
||||
line = line.strip()
|
||||
if not line.startswith("data:"):
|
||||
continue
|
||||
payload = line[len("data:") :].strip()
|
||||
if payload == "[DONE]" or not payload:
|
||||
continue
|
||||
try:
|
||||
parsed = cast(object, json.loads(payload))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
event = as_dict(parsed)
|
||||
if event is None:
|
||||
continue
|
||||
self._consume_event(event)
|
||||
|
||||
def _consume_event(self, event: dict[str, object]) -> None:
|
||||
choices = dict_get_list(event, "choices")
|
||||
if not choices:
|
||||
return
|
||||
for choice_raw in choices:
|
||||
choice = as_dict(choice_raw)
|
||||
if choice is None:
|
||||
continue
|
||||
index_val = choice.get("index", 0)
|
||||
if (
|
||||
not (isinstance(index_val, int) and not isinstance(index_val, bool))
|
||||
or index_val != 0
|
||||
):
|
||||
continue
|
||||
delta = dict_get_dict(choice, "delta")
|
||||
if delta is None:
|
||||
continue
|
||||
content = dict_get_str(delta, "content")
|
||||
if content is not None:
|
||||
self._content_parts.append(content)
|
||||
reasoning = dict_get_str(delta, "reasoning_content")
|
||||
if reasoning is not None:
|
||||
self._reasoning_parts.append(reasoning)
|
||||
tool_calls = dict_get_list(delta, "tool_calls")
|
||||
if tool_calls is not None:
|
||||
self._merge_tool_calls(tool_calls)
|
||||
|
||||
def _merge_tool_calls(self, deltas: list[object]) -> None:
|
||||
for raw in deltas:
|
||||
d = as_dict(raw)
|
||||
if d is None:
|
||||
continue
|
||||
index_val = d.get("index", 0)
|
||||
if not (isinstance(index_val, int) and not isinstance(index_val, bool)):
|
||||
continue
|
||||
entry = self._tool_calls_by_index.setdefault(
|
||||
index_val,
|
||||
{
|
||||
"id": "",
|
||||
"type": "function",
|
||||
"function": {"name": "", "arguments": ""},
|
||||
},
|
||||
)
|
||||
tc_id = dict_get_str(d, "id")
|
||||
if tc_id is not None:
|
||||
entry["id"] = tc_id
|
||||
tc_type = dict_get_str(d, "type")
|
||||
if tc_type is not None:
|
||||
entry["type"] = tc_type
|
||||
fn = dict_get_dict(d, "function")
|
||||
if fn is not None:
|
||||
entry_fn = entry.get("function")
|
||||
if not isinstance(entry_fn, dict):
|
||||
entry_fn = {"name": "", "arguments": ""}
|
||||
entry["function"] = entry_fn
|
||||
entry_fn_typed = cast(dict[str, object], entry_fn)
|
||||
name = dict_get_str(fn, "name")
|
||||
if name is not None:
|
||||
prev_name = as_str(entry_fn_typed.get("name")) or ""
|
||||
entry_fn_typed["name"] = prev_name + name
|
||||
args = dict_get_str(fn, "arguments")
|
||||
if args is not None:
|
||||
prev_args = as_str(entry_fn_typed.get("arguments")) or ""
|
||||
entry_fn_typed["arguments"] = prev_args + args
|
||||
|
||||
@property
|
||||
def content(self) -> str | None:
|
||||
joined = "".join(self._content_parts)
|
||||
return joined if joined else None
|
||||
|
||||
@property
|
||||
def tool_calls(self) -> list[dict[str, object]] | None:
|
||||
if not self._tool_calls_by_index:
|
||||
return None
|
||||
ordered = [
|
||||
self._tool_calls_by_index[i] for i in sorted(self._tool_calls_by_index)
|
||||
]
|
||||
return ordered
|
||||
|
||||
@property
|
||||
def reasoning(self) -> str:
|
||||
return "".join(self._reasoning_parts)
|
||||
|
||||
|
||||
class ClaudeAccumulator:
|
||||
"""Captures Claude streaming content blocks.
|
||||
|
||||
Tracks per-index content blocks. At end, exposes the final `content_blocks`
|
||||
list (excluding thinking blocks — those go into `reasoning` as joined text)
|
||||
in a shape suitable for hashing.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._blocks_by_index: dict[int, dict[str, object]] = {}
|
||||
self._buffer = ""
|
||||
self._current_event: str | None = None
|
||||
|
||||
def feed_bytes(self, chunk: bytes) -> None:
|
||||
self._buffer += chunk.decode("utf-8", errors="replace")
|
||||
while "\n" in self._buffer:
|
||||
line, self._buffer = self._buffer.split("\n", 1)
|
||||
line = line.rstrip("\r")
|
||||
if not line:
|
||||
self._current_event = None
|
||||
continue
|
||||
if line.startswith("event:"):
|
||||
self._current_event = line[len("event:") :].strip()
|
||||
continue
|
||||
if line.startswith("data:"):
|
||||
payload = line[len("data:") :].strip()
|
||||
try:
|
||||
parsed = cast(object, json.loads(payload))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
event = as_dict(parsed)
|
||||
if event is None:
|
||||
continue
|
||||
self._consume_event(event)
|
||||
|
||||
def _consume_event(self, event: dict[str, object]) -> None:
|
||||
event_type = dict_get_str(event, "type") or self._current_event
|
||||
if event_type == "content_block_start":
|
||||
index_val = event.get("index", 0)
|
||||
if not (isinstance(index_val, int) and not isinstance(index_val, bool)):
|
||||
return
|
||||
block = dict_get_dict(event, "content_block")
|
||||
if block is None:
|
||||
return
|
||||
btype = dict_get_str(block, "type")
|
||||
if btype == "text":
|
||||
self._blocks_by_index[index_val] = {"type": "text", "text": ""}
|
||||
elif btype == "thinking":
|
||||
self._blocks_by_index[index_val] = {
|
||||
"type": "thinking",
|
||||
"thinking": "",
|
||||
}
|
||||
elif btype == "tool_use":
|
||||
self._blocks_by_index[index_val] = {
|
||||
"type": "tool_use",
|
||||
"id": dict_get_str(block, "id") or "",
|
||||
"name": dict_get_str(block, "name") or "",
|
||||
"input_json": "",
|
||||
}
|
||||
elif event_type == "content_block_delta":
|
||||
index_val = event.get("index", 0)
|
||||
if not (isinstance(index_val, int) and not isinstance(index_val, bool)):
|
||||
return
|
||||
delta = dict_get_dict(event, "delta")
|
||||
if delta is None:
|
||||
return
|
||||
block = self._blocks_by_index.get(index_val)
|
||||
if block is None:
|
||||
return
|
||||
dtype = dict_get_str(delta, "type")
|
||||
if dtype == "text_delta":
|
||||
text = dict_get_str(delta, "text")
|
||||
if text is not None:
|
||||
prev = as_str(block.get("text")) or ""
|
||||
block["text"] = prev + text
|
||||
elif dtype == "thinking_delta":
|
||||
thinking = dict_get_str(delta, "thinking")
|
||||
if thinking is not None:
|
||||
prev = as_str(block.get("thinking")) or ""
|
||||
block["thinking"] = prev + thinking
|
||||
elif dtype == "input_json_delta":
|
||||
partial = dict_get_str(delta, "partial_json")
|
||||
if partial is not None:
|
||||
prev = as_str(block.get("input_json")) or ""
|
||||
block["input_json"] = prev + partial
|
||||
|
||||
@property
|
||||
def content_blocks(self) -> list[dict[str, object]]:
|
||||
"""Public blocks (excludes thinking), with tool_use input parsed from JSON."""
|
||||
public: list[dict[str, object]] = []
|
||||
for index in sorted(self._blocks_by_index):
|
||||
block = self._blocks_by_index[index]
|
||||
if block.get("type") == "thinking":
|
||||
continue
|
||||
if block.get("type") == "tool_use":
|
||||
input_json = as_str(block.get("input_json")) or "{}"
|
||||
parsed_input_raw: object
|
||||
try:
|
||||
parsed_input_raw = cast(object, json.loads(input_json))
|
||||
except json.JSONDecodeError:
|
||||
parsed_input_raw = {}
|
||||
parsed_input = as_dict(parsed_input_raw) or {}
|
||||
public.append(
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": as_str(block.get("id")) or "",
|
||||
"name": as_str(block.get("name")) or "",
|
||||
"input": parsed_input,
|
||||
}
|
||||
)
|
||||
else:
|
||||
public.append({k: v for k, v in block.items() if k != "input_json"})
|
||||
return public
|
||||
|
||||
@property
|
||||
def reasoning(self) -> str:
|
||||
parts: list[str] = []
|
||||
for index in sorted(self._blocks_by_index):
|
||||
block = self._blocks_by_index[index]
|
||||
if block.get("type") == "thinking":
|
||||
parts.append(as_str(block.get("thinking")) or "")
|
||||
return "".join(parts)
|
||||
@@ -0,0 +1,21 @@
|
||||
import threading
|
||||
|
||||
|
||||
class ReasoningCache:
|
||||
def __init__(self) -> None:
|
||||
self._store: dict[str, str] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def get(self, content_hash: str) -> str | None:
|
||||
with self._lock:
|
||||
return self._store.get(content_hash)
|
||||
|
||||
def put(self, content_hash: str, reasoning: str) -> None:
|
||||
if not reasoning:
|
||||
return
|
||||
with self._lock:
|
||||
self._store[content_hash] = reasoning
|
||||
|
||||
def size(self) -> int:
|
||||
with self._lock:
|
||||
return len(self._store)
|
||||
@@ -0,0 +1,170 @@
|
||||
"""Dialect strategies for deciding which assistant history indices should receive
|
||||
cached reasoning on inbound requests.
|
||||
|
||||
Each dialect inspects the message list and returns the set of indices where
|
||||
reasoning_content (OpenAI) or a thinking block (Claude) should be reattached if
|
||||
the cache has it. The dialect does not mutate messages — the caller does.
|
||||
|
||||
Dialect selection is driven by the `reasoning_dialect` field on each model card,
|
||||
surfaced through /v1/models.
|
||||
"""
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
from exo.reasoning_proxy._helpers import as_dict, as_list, dict_get_list, dict_get_str
|
||||
from exo.shared.types.text_generation import ReasoningDialect
|
||||
|
||||
|
||||
class Dialect(Protocol):
|
||||
def select_attach_indices(
|
||||
self, messages: list[dict[str, object]], has_tools: bool
|
||||
) -> set[int]: ...
|
||||
|
||||
|
||||
def _is_assistant(msg: dict[str, object]) -> bool:
|
||||
return msg.get("role") == "assistant"
|
||||
|
||||
|
||||
def _is_user(msg: dict[str, object]) -> bool:
|
||||
return msg.get("role") == "user"
|
||||
|
||||
|
||||
class NoneDialect:
|
||||
def select_attach_indices(
|
||||
self, messages: list[dict[str, object]], has_tools: bool
|
||||
) -> set[int]:
|
||||
return set()
|
||||
|
||||
|
||||
class PostLastUserDialect:
|
||||
"""MiniMax / GLM / Qwen-thinking / V4-with-tools.
|
||||
|
||||
Preserve reasoning on every assistant message appearing after the last
|
||||
non-tool-response user message. Tool-response user messages (role=tool, or
|
||||
role=user with tool_call_id set, or Claude's tool_result block) don't count
|
||||
as "real" user turns — they're part of the assistant's tool-calling chain.
|
||||
"""
|
||||
|
||||
def select_attach_indices(
|
||||
self, messages: list[dict[str, object]], has_tools: bool
|
||||
) -> set[int]:
|
||||
last_user_index = -1
|
||||
for i, msg in enumerate(messages):
|
||||
if _is_user(msg) and not _is_tool_response(msg):
|
||||
last_user_index = i
|
||||
return {
|
||||
i
|
||||
for i, msg in enumerate(messages)
|
||||
if i > last_user_index and _is_assistant(msg)
|
||||
}
|
||||
|
||||
|
||||
class SuffixDialect:
|
||||
"""Kimi K2 Thinking / K2.6.
|
||||
|
||||
Preserve reasoning only on the tail run of tool-call-carrying assistant
|
||||
messages (the current, unresolved tool-call chain). Walk backward: include
|
||||
every assistant with tool_calls until we hit an assistant without tool_calls
|
||||
or a non-assistant message.
|
||||
"""
|
||||
|
||||
def select_attach_indices(
|
||||
self, messages: list[dict[str, object]], has_tools: bool
|
||||
) -> set[int]:
|
||||
indices: set[int] = set()
|
||||
for i in range(len(messages) - 1, -1, -1):
|
||||
msg = messages[i]
|
||||
if not _is_assistant(msg):
|
||||
if _is_tool_response(msg):
|
||||
continue
|
||||
break
|
||||
if not _has_tool_calls(msg):
|
||||
break
|
||||
indices.add(i)
|
||||
return indices
|
||||
|
||||
|
||||
class ChannelDialect:
|
||||
"""GPT-OSS Harmony format.
|
||||
|
||||
Preserve analysis-channel content on assistant turns that follow the most
|
||||
recent assistant message tagged with a "final" channel marker. If no prior
|
||||
final exists, the whole conversation is one unresolved chain.
|
||||
"""
|
||||
|
||||
def select_attach_indices(
|
||||
self, messages: list[dict[str, object]], has_tools: bool
|
||||
) -> set[int]:
|
||||
last_final_index = -1
|
||||
for i, msg in enumerate(messages):
|
||||
if _is_assistant(msg) and _has_final_channel(msg):
|
||||
last_final_index = i
|
||||
return {
|
||||
i
|
||||
for i, msg in enumerate(messages)
|
||||
if i > last_final_index and _is_assistant(msg)
|
||||
}
|
||||
|
||||
|
||||
class ToolConditionalDialect:
|
||||
"""DeepSeek V4 Flash.
|
||||
|
||||
If the request has tools, behave as PostLastUserDialect; otherwise passthrough.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._inner = PostLastUserDialect()
|
||||
|
||||
def select_attach_indices(
|
||||
self, messages: list[dict[str, object]], has_tools: bool
|
||||
) -> set[int]:
|
||||
if not has_tools:
|
||||
return set()
|
||||
return self._inner.select_attach_indices(messages, has_tools)
|
||||
|
||||
|
||||
def _is_tool_response(msg: dict[str, object]) -> bool:
|
||||
if msg.get("role") == "tool":
|
||||
return True
|
||||
if msg.get("role") == "user" and msg.get("tool_call_id"):
|
||||
return True
|
||||
content = as_list(msg.get("content"))
|
||||
if content is not None:
|
||||
for raw in content:
|
||||
block = as_dict(raw)
|
||||
if block is not None and block.get("type") == "tool_result":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_tool_calls(msg: dict[str, object]) -> bool:
|
||||
tc = dict_get_list(msg, "tool_calls")
|
||||
if tc:
|
||||
return True
|
||||
content = as_list(msg.get("content"))
|
||||
if content is not None:
|
||||
for raw in content:
|
||||
block = as_dict(raw)
|
||||
if block is not None and block.get("type") == "tool_use":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_final_channel(msg: dict[str, object]) -> bool:
|
||||
if msg.get("channel") == "final":
|
||||
return True
|
||||
content = dict_get_str(msg, "content")
|
||||
return bool(content and content.strip())
|
||||
|
||||
|
||||
_DIALECTS: dict[ReasoningDialect, Dialect] = {
|
||||
"none": NoneDialect(),
|
||||
"post_last_user": PostLastUserDialect(),
|
||||
"suffix": SuffixDialect(),
|
||||
"channel": ChannelDialect(),
|
||||
"tool_conditional": ToolConditionalDialect(),
|
||||
}
|
||||
|
||||
|
||||
def get_dialect(name: ReasoningDialect) -> Dialect:
|
||||
return _DIALECTS[name]
|
||||
@@ -0,0 +1,75 @@
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from exo.reasoning_proxy._helpers import as_dict, dict_get_str
|
||||
|
||||
|
||||
def _canonical_tool_calls(
|
||||
tool_calls: list[dict[str, object]] | None,
|
||||
) -> list[dict[str, object]]:
|
||||
if not tool_calls:
|
||||
return []
|
||||
result: list[dict[str, object]] = []
|
||||
for tc in tool_calls:
|
||||
entry: dict[str, object] = {}
|
||||
if "id" in tc:
|
||||
entry["id"] = tc["id"]
|
||||
fn = as_dict(tc.get("function"))
|
||||
if fn is not None:
|
||||
entry["function"] = {
|
||||
"name": dict_get_str(fn, "name") or "",
|
||||
"arguments": dict_get_str(fn, "arguments") or "",
|
||||
}
|
||||
if "type" in tc:
|
||||
entry["type"] = tc["type"]
|
||||
result.append(entry)
|
||||
return result
|
||||
|
||||
|
||||
def hash_openai_assistant(
|
||||
content: str | list[object] | None,
|
||||
tool_calls: list[dict[str, object]] | None,
|
||||
) -> str:
|
||||
"""Deterministic hash of an OpenAI assistant message's observable surface.
|
||||
|
||||
Canonicalizes None content to "" and tool_calls to a minimal id/function shape
|
||||
so trivial shape differences between client render and our re-emit don't miss.
|
||||
"""
|
||||
shape: dict[str, object] = {
|
||||
"content": content if content is not None else "",
|
||||
"tool_calls": _canonical_tool_calls(tool_calls),
|
||||
}
|
||||
payload = json.dumps(
|
||||
shape, sort_keys=True, ensure_ascii=False, separators=(",", ":")
|
||||
)
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def hash_claude_assistant(content_blocks: list[dict[str, object]]) -> str:
|
||||
"""Deterministic hash of a Claude assistant message's observable surface.
|
||||
|
||||
Skips thinking blocks (we're hashing what the *client sends back*, which typically
|
||||
omits thinking) and normalizes tool_use blocks to id/name/input.
|
||||
"""
|
||||
normalized: list[dict[str, object]] = []
|
||||
for block in content_blocks:
|
||||
btype = block.get("type")
|
||||
if btype == "text":
|
||||
normalized.append(
|
||||
{"type": "text", "text": dict_get_str(block, "text") or ""}
|
||||
)
|
||||
elif btype == "tool_use":
|
||||
normalized.append(
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": dict_get_str(block, "id") or "",
|
||||
"name": dict_get_str(block, "name") or "",
|
||||
"input": block.get("input")
|
||||
if block.get("input") is not None
|
||||
else {},
|
||||
}
|
||||
)
|
||||
payload = json.dumps(
|
||||
normalized, sort_keys=True, ensure_ascii=False, separators=(",", ":")
|
||||
)
|
||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
@@ -0,0 +1,70 @@
|
||||
import argparse
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import cast
|
||||
|
||||
import httpx
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
|
||||
from exo.reasoning_proxy.cache import ReasoningCache
|
||||
from exo.reasoning_proxy.registry import DialectRegistry
|
||||
from exo.reasoning_proxy.routes import register_routes
|
||||
|
||||
logger = logging.getLogger("exo.reasoning_proxy")
|
||||
|
||||
|
||||
def build_app(upstream: str) -> FastAPI:
|
||||
client = httpx.AsyncClient(timeout=httpx.Timeout(None, connect=10.0))
|
||||
cache = ReasoningCache()
|
||||
registry = DialectRegistry(upstream=upstream, client=client)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
await registry.refresh()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await client.aclose()
|
||||
|
||||
app = FastAPI(lifespan=lifespan, title="exo-reasoning-proxy")
|
||||
register_routes(
|
||||
app=app,
|
||||
client=client,
|
||||
upstream=upstream.rstrip("/"),
|
||||
cache=cache,
|
||||
registry=registry,
|
||||
)
|
||||
return app
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(prog="exo-reasoning-proxy")
|
||||
_ = parser.add_argument("--upstream", default="http://localhost:52415")
|
||||
_ = parser.add_argument("--host", default="127.0.0.1")
|
||||
_ = parser.add_argument("--port", type=int, default=52416)
|
||||
_ = parser.add_argument("-v", "--verbose", action="count", default=0)
|
||||
args = parser.parse_args()
|
||||
|
||||
verbose = cast(int, args.verbose)
|
||||
upstream = cast(str, args.upstream)
|
||||
host = cast(str, args.host)
|
||||
port = cast(int, args.port)
|
||||
|
||||
level = logging.WARNING
|
||||
if verbose == 1:
|
||||
level = logging.INFO
|
||||
elif verbose >= 2:
|
||||
level = logging.DEBUG
|
||||
logging.basicConfig(
|
||||
level=level, format="%(asctime)s %(levelname)s %(name)s: %(message)s"
|
||||
)
|
||||
|
||||
logger.info("Starting exo-reasoning-proxy on %s:%d → %s", host, port, upstream)
|
||||
|
||||
app = build_app(upstream=upstream)
|
||||
uvicorn.run(app, host=host, port=port, log_level=level)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,75 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import cast, get_args
|
||||
|
||||
import httpx
|
||||
|
||||
from exo.reasoning_proxy._helpers import as_dict, as_list, dict_get_str
|
||||
from exo.shared.types.text_generation import ReasoningDialect
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DialectRegistry:
|
||||
def __init__(self, upstream: str, client: httpx.AsyncClient) -> None:
|
||||
self._upstream = upstream.rstrip("/")
|
||||
self._client = client
|
||||
self._by_model: dict[str, ReasoningDialect] = {}
|
||||
self._unknown_logged: set[str] = set()
|
||||
self._lock = asyncio.Lock()
|
||||
self._initialized = False
|
||||
|
||||
async def refresh(self) -> None:
|
||||
await self._fetch()
|
||||
|
||||
async def _fetch(self) -> None:
|
||||
try:
|
||||
resp = await self._client.get(f"{self._upstream}/v1/models", timeout=10.0)
|
||||
resp.raise_for_status()
|
||||
body = as_dict(cast(object, resp.json()))
|
||||
if body is None:
|
||||
return
|
||||
data = as_list(body.get("data")) or []
|
||||
updated: dict[str, ReasoningDialect] = {}
|
||||
for entry_raw in data:
|
||||
entry = as_dict(entry_raw)
|
||||
if entry is None:
|
||||
continue
|
||||
model_id = dict_get_str(entry, "id")
|
||||
dialect_raw = entry.get("reasoning_dialect", "none")
|
||||
if model_id is not None:
|
||||
updated[model_id] = _coerce_dialect(dialect_raw)
|
||||
self._by_model = updated
|
||||
self._initialized = True
|
||||
logger.info(
|
||||
"Loaded %d model dialects from %s", len(updated), self._upstream
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to fetch /v1/models from %s: %s", self._upstream, exc
|
||||
)
|
||||
|
||||
async def resolve(self, model_id: str) -> ReasoningDialect:
|
||||
async with self._lock:
|
||||
if not self._initialized:
|
||||
await self._fetch()
|
||||
if model_id in self._by_model:
|
||||
return self._by_model[model_id]
|
||||
await self._fetch()
|
||||
if model_id in self._by_model:
|
||||
return self._by_model[model_id]
|
||||
if model_id not in self._unknown_logged:
|
||||
logger.info(
|
||||
"No dialect declared for model %s; passing through", model_id
|
||||
)
|
||||
self._unknown_logged.add(model_id)
|
||||
return "none"
|
||||
|
||||
|
||||
_VALID_DIALECTS: frozenset[str] = frozenset(get_args(ReasoningDialect))
|
||||
|
||||
|
||||
def _coerce_dialect(value: object) -> ReasoningDialect:
|
||||
if isinstance(value, str) and value in _VALID_DIALECTS:
|
||||
return cast(ReasoningDialect, value)
|
||||
return "none"
|
||||
@@ -0,0 +1,384 @@
|
||||
"""FastAPI handlers for the reasoning proxy.
|
||||
|
||||
Two handlers, one shape: read body → resolve dialect → reattach cached
|
||||
reasoning to designated history indices → forward → tee the response stream
|
||||
→ capture emitted reasoning → cache under the emitted assistant's hash.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import cast
|
||||
|
||||
import httpx
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
||||
from starlette.datastructures import Headers as StarletteHeaders
|
||||
|
||||
from exo.reasoning_proxy._helpers import (
|
||||
as_dict,
|
||||
as_list,
|
||||
dict_get_dict,
|
||||
dict_get_list,
|
||||
dict_get_str,
|
||||
)
|
||||
from exo.reasoning_proxy.accumulator import ClaudeAccumulator, OpenAIAccumulator
|
||||
from exo.reasoning_proxy.cache import ReasoningCache
|
||||
from exo.reasoning_proxy.dialects import get_dialect
|
||||
from exo.reasoning_proxy.hashing import hash_claude_assistant, hash_openai_assistant
|
||||
from exo.reasoning_proxy.registry import DialectRegistry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _parse_body(raw_body: bytes) -> dict[str, object] | None:
|
||||
try:
|
||||
parsed = cast(object, json.loads(raw_body))
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return as_dict(parsed)
|
||||
|
||||
|
||||
def _content_for_hash(value: object) -> str | list[object] | None:
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
as_listed = as_list(value)
|
||||
if as_listed is not None:
|
||||
return as_listed
|
||||
return None
|
||||
|
||||
|
||||
def _attach_openai_reasoning(
|
||||
messages: list[dict[str, object]],
|
||||
indices: set[int],
|
||||
cache: ReasoningCache,
|
||||
) -> None:
|
||||
for i in indices:
|
||||
msg = messages[i]
|
||||
existing = dict_get_str(msg, "reasoning_content")
|
||||
if existing:
|
||||
continue
|
||||
content = _content_for_hash(msg.get("content"))
|
||||
tool_calls_raw = dict_get_list(msg, "tool_calls") or []
|
||||
tool_calls: list[dict[str, object]] = [
|
||||
d for d in (as_dict(t) for t in tool_calls_raw) if d is not None
|
||||
]
|
||||
h = hash_openai_assistant(content, tool_calls or None)
|
||||
cached = cache.get(h)
|
||||
if cached is not None:
|
||||
msg["reasoning_content"] = cached
|
||||
|
||||
|
||||
def _attach_claude_reasoning(
|
||||
messages: list[dict[str, object]],
|
||||
indices: set[int],
|
||||
cache: ReasoningCache,
|
||||
) -> None:
|
||||
for i in indices:
|
||||
msg = messages[i]
|
||||
content = as_list(msg.get("content"))
|
||||
if content is None:
|
||||
continue
|
||||
has_thinking = False
|
||||
normalized: list[dict[str, object]] = []
|
||||
for raw in content:
|
||||
block = as_dict(raw)
|
||||
if block is None:
|
||||
continue
|
||||
if block.get("type") == "thinking":
|
||||
has_thinking = True
|
||||
normalized.append(block)
|
||||
if has_thinking:
|
||||
continue
|
||||
h = hash_claude_assistant(normalized)
|
||||
cached = cache.get(h)
|
||||
if cached is None:
|
||||
continue
|
||||
new_content: list[dict[str, object]] = [
|
||||
{"type": "thinking", "thinking": cached}
|
||||
]
|
||||
new_content.extend(normalized)
|
||||
msg["content"] = new_content
|
||||
|
||||
|
||||
async def _stream_and_capture_openai(
|
||||
upstream_resp: httpx.Response,
|
||||
cache: ReasoningCache,
|
||||
) -> AsyncIterator[bytes]:
|
||||
accumulator = OpenAIAccumulator()
|
||||
try:
|
||||
async for chunk in upstream_resp.aiter_raw():
|
||||
accumulator.feed_bytes(chunk)
|
||||
yield chunk
|
||||
finally:
|
||||
await upstream_resp.aclose()
|
||||
reasoning = accumulator.reasoning
|
||||
if not reasoning:
|
||||
return
|
||||
h = hash_openai_assistant(accumulator.content, accumulator.tool_calls)
|
||||
cache.put(h, reasoning)
|
||||
|
||||
|
||||
async def _stream_and_capture_claude(
|
||||
upstream_resp: httpx.Response,
|
||||
cache: ReasoningCache,
|
||||
) -> AsyncIterator[bytes]:
|
||||
accumulator = ClaudeAccumulator()
|
||||
try:
|
||||
async for chunk in upstream_resp.aiter_raw():
|
||||
accumulator.feed_bytes(chunk)
|
||||
yield chunk
|
||||
finally:
|
||||
await upstream_resp.aclose()
|
||||
reasoning = accumulator.reasoning
|
||||
if not reasoning:
|
||||
return
|
||||
h = hash_claude_assistant(accumulator.content_blocks)
|
||||
cache.put(h, reasoning)
|
||||
|
||||
|
||||
def _capture_openai_nonstream(body_text: str, cache: ReasoningCache) -> None:
|
||||
body = _parse_body(body_text.encode("utf-8"))
|
||||
if body is None:
|
||||
return
|
||||
choices = dict_get_list(body, "choices")
|
||||
if not choices:
|
||||
return
|
||||
first = as_dict(choices[0])
|
||||
if first is None:
|
||||
return
|
||||
message = dict_get_dict(first, "message")
|
||||
if message is None:
|
||||
return
|
||||
reasoning = dict_get_str(message, "reasoning_content")
|
||||
if not reasoning:
|
||||
return
|
||||
content = _content_for_hash(message.get("content"))
|
||||
tool_calls_raw = dict_get_list(message, "tool_calls") or []
|
||||
tool_calls: list[dict[str, object]] = [
|
||||
d for d in (as_dict(t) for t in tool_calls_raw) if d is not None
|
||||
]
|
||||
h = hash_openai_assistant(content, tool_calls or None)
|
||||
cache.put(h, reasoning)
|
||||
|
||||
|
||||
def _capture_claude_nonstream(body_text: str, cache: ReasoningCache) -> None:
|
||||
body = _parse_body(body_text.encode("utf-8"))
|
||||
if body is None:
|
||||
return
|
||||
content = as_list(body.get("content"))
|
||||
if content is None:
|
||||
return
|
||||
reasoning_parts: list[str] = []
|
||||
public_blocks: list[dict[str, object]] = []
|
||||
for raw in content:
|
||||
block = as_dict(raw)
|
||||
if block is None:
|
||||
continue
|
||||
if block.get("type") == "thinking":
|
||||
thinking = dict_get_str(block, "thinking")
|
||||
if thinking is not None:
|
||||
reasoning_parts.append(thinking)
|
||||
else:
|
||||
public_blocks.append(block)
|
||||
reasoning = "".join(reasoning_parts)
|
||||
if not reasoning:
|
||||
return
|
||||
h = hash_claude_assistant(public_blocks)
|
||||
cache.put(h, reasoning)
|
||||
|
||||
|
||||
def _messages_from_body(body: dict[str, object]) -> list[dict[str, object]] | None:
|
||||
raw = as_list(body.get("messages"))
|
||||
if raw is None:
|
||||
return None
|
||||
result: list[dict[str, object]] = []
|
||||
for item in raw:
|
||||
m = as_dict(item)
|
||||
if m is None:
|
||||
return None
|
||||
result.append(m)
|
||||
return result
|
||||
|
||||
|
||||
def register_routes(
|
||||
app: FastAPI,
|
||||
client: httpx.AsyncClient,
|
||||
upstream: str,
|
||||
cache: ReasoningCache,
|
||||
registry: DialectRegistry,
|
||||
) -> None:
|
||||
async def handle_chat_completions(request: Request) -> Response:
|
||||
raw_body = await request.body()
|
||||
body = _parse_body(raw_body)
|
||||
if body is None:
|
||||
return _bad_request("invalid JSON body")
|
||||
|
||||
model_id = dict_get_str(body, "model")
|
||||
if model_id is None:
|
||||
return _bad_request("missing or invalid 'model' field")
|
||||
|
||||
dialect_name = await registry.resolve(model_id)
|
||||
dialect = get_dialect(dialect_name)
|
||||
|
||||
messages = _messages_from_body(body)
|
||||
if messages is not None:
|
||||
has_tools = bool(body.get("tools"))
|
||||
indices = dialect.select_attach_indices(messages, has_tools=has_tools)
|
||||
if indices:
|
||||
_attach_openai_reasoning(messages, indices, cache)
|
||||
body["messages"] = messages
|
||||
|
||||
forward_body = json.dumps(body).encode("utf-8")
|
||||
forward_headers = _copy_headers(request.headers)
|
||||
forward_headers["content-length"] = str(len(forward_body))
|
||||
|
||||
is_stream = bool(body.get("stream"))
|
||||
|
||||
try:
|
||||
if is_stream:
|
||||
req = client.build_request(
|
||||
"POST",
|
||||
f"{upstream}/v1/chat/completions",
|
||||
content=forward_body,
|
||||
headers=forward_headers,
|
||||
)
|
||||
upstream_resp = await client.send(req, stream=True)
|
||||
return StreamingResponse(
|
||||
_stream_and_capture_openai(upstream_resp, cache),
|
||||
status_code=upstream_resp.status_code,
|
||||
media_type=_media_type(upstream_resp.headers, "text/event-stream"),
|
||||
headers=_response_headers(upstream_resp.headers),
|
||||
)
|
||||
upstream_resp = await client.post(
|
||||
f"{upstream}/v1/chat/completions",
|
||||
content=forward_body,
|
||||
headers=forward_headers,
|
||||
)
|
||||
text = upstream_resp.text
|
||||
if upstream_resp.status_code == 200:
|
||||
_capture_openai_nonstream(text, cache)
|
||||
return Response(
|
||||
content=text,
|
||||
status_code=upstream_resp.status_code,
|
||||
media_type=_media_type(upstream_resp.headers, "application/json"),
|
||||
headers=_response_headers(upstream_resp.headers),
|
||||
)
|
||||
except httpx.RequestError as exc:
|
||||
logger.warning("Upstream request failed: %s", exc)
|
||||
return _bad_gateway(str(exc))
|
||||
|
||||
async def handle_claude_messages(request: Request) -> Response:
|
||||
raw_body = await request.body()
|
||||
body = _parse_body(raw_body)
|
||||
if body is None:
|
||||
return _bad_request("invalid JSON body")
|
||||
|
||||
model_id = dict_get_str(body, "model")
|
||||
if model_id is None:
|
||||
return _bad_request("missing or invalid 'model' field")
|
||||
|
||||
dialect_name = await registry.resolve(model_id)
|
||||
dialect = get_dialect(dialect_name)
|
||||
|
||||
messages = _messages_from_body(body)
|
||||
if messages is not None:
|
||||
has_tools = bool(body.get("tools"))
|
||||
indices = dialect.select_attach_indices(messages, has_tools=has_tools)
|
||||
if indices:
|
||||
_attach_claude_reasoning(messages, indices, cache)
|
||||
body["messages"] = messages
|
||||
|
||||
forward_body = json.dumps(body).encode("utf-8")
|
||||
forward_headers = _copy_headers(request.headers)
|
||||
forward_headers["content-length"] = str(len(forward_body))
|
||||
|
||||
is_stream = bool(body.get("stream"))
|
||||
|
||||
try:
|
||||
if is_stream:
|
||||
req = client.build_request(
|
||||
"POST",
|
||||
f"{upstream}/v1/messages",
|
||||
content=forward_body,
|
||||
headers=forward_headers,
|
||||
)
|
||||
upstream_resp = await client.send(req, stream=True)
|
||||
return StreamingResponse(
|
||||
_stream_and_capture_claude(upstream_resp, cache),
|
||||
status_code=upstream_resp.status_code,
|
||||
media_type=_media_type(upstream_resp.headers, "text/event-stream"),
|
||||
headers=_response_headers(upstream_resp.headers),
|
||||
)
|
||||
upstream_resp = await client.post(
|
||||
f"{upstream}/v1/messages",
|
||||
content=forward_body,
|
||||
headers=forward_headers,
|
||||
)
|
||||
text = upstream_resp.text
|
||||
if upstream_resp.status_code == 200:
|
||||
_capture_claude_nonstream(text, cache)
|
||||
return Response(
|
||||
content=text,
|
||||
status_code=upstream_resp.status_code,
|
||||
media_type=_media_type(upstream_resp.headers, "application/json"),
|
||||
headers=_response_headers(upstream_resp.headers),
|
||||
)
|
||||
except httpx.RequestError as exc:
|
||||
logger.warning("Upstream request failed: %s", exc)
|
||||
return _bad_gateway(str(exc))
|
||||
|
||||
async def health() -> dict[str, object]:
|
||||
return {"status": "ok", "cache_entries": cache.size()}
|
||||
|
||||
_ = app.post("/v1/chat/completions")(handle_chat_completions)
|
||||
_ = app.post("/v1/messages")(handle_claude_messages)
|
||||
_ = app.get("/health")(health)
|
||||
|
||||
|
||||
_HOP_BY_HOP = {
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailers",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
"content-length",
|
||||
"host",
|
||||
}
|
||||
|
||||
|
||||
def _copy_headers(headers: httpx.Headers | StarletteHeaders) -> dict[str, str]:
|
||||
out: dict[str, str] = {}
|
||||
for k, v in headers.items():
|
||||
if k.lower() in _HOP_BY_HOP:
|
||||
continue
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def _response_headers(headers: httpx.Headers) -> dict[str, str]:
|
||||
return _copy_headers(headers)
|
||||
|
||||
|
||||
def _media_type(headers: httpx.Headers, default: str) -> str:
|
||||
value = cast(object, headers.get("content-type", default))
|
||||
return value if isinstance(value, str) else default
|
||||
|
||||
|
||||
def _bad_request(msg: str) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"error": {"message": msg, "type": "invalid_request_error"}},
|
||||
)
|
||||
|
||||
|
||||
def _bad_gateway(msg: str) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=502,
|
||||
content={
|
||||
"error": {"message": f"upstream unreachable: {msg}", "type": "bad_gateway"}
|
||||
},
|
||||
)
|
||||
@@ -28,6 +28,7 @@ from exo.shared.constants import (
|
||||
)
|
||||
from exo.shared.types.common import ModelId
|
||||
from exo.shared.types.memory import Memory
|
||||
from exo.shared.types.text_generation import ReasoningDialect
|
||||
from exo.utils.pydantic_ext import FrozenModel
|
||||
|
||||
# kinda ugly...
|
||||
@@ -145,6 +146,7 @@ class ModelCard(FrozenModel):
|
||||
quantization: str = ""
|
||||
base_model: str = ""
|
||||
capabilities: list[str] = []
|
||||
reasoning_dialect: ReasoningDialect = "none"
|
||||
context_length: int = 0
|
||||
uses_cfg: bool = False
|
||||
trust_remote_code: bool = True
|
||||
@@ -270,6 +272,7 @@ class ConfigData(BaseModel):
|
||||
return self.architectures in [
|
||||
["Glm4MoeLiteForCausalLM"],
|
||||
["GlmMoeDsaForCausalLM"],
|
||||
["DeepseekV4ForCausalLM"],
|
||||
["DeepseekV32ForCausalLM"],
|
||||
["DeepseekV3ForCausalLM"],
|
||||
["Qwen3NextForCausalLM"],
|
||||
|
||||
@@ -13,6 +13,9 @@ from exo.shared.types.common import ModelId, TruncatingString
|
||||
|
||||
MessageRole = Literal["user", "assistant", "system", "developer", "tool"]
|
||||
ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh"]
|
||||
ReasoningDialect = Literal[
|
||||
"none", "post_last_user", "suffix", "channel", "tool_conditional"
|
||||
]
|
||||
|
||||
|
||||
def resolve_reasoning_params(
|
||||
|
||||
@@ -17,6 +17,8 @@ from mlx_lm.models.base import (
|
||||
from mlx_lm.models.cache import ArraysCache, KVCache
|
||||
from mlx_lm.models.deepseek_v3 import DeepseekV3MLP
|
||||
from mlx_lm.models.deepseek_v3 import Model as DeepseekV3Model
|
||||
from mlx_lm.models.deepseek_v4 import DeepseekV4MoE, V4Attention
|
||||
from mlx_lm.models.deepseek_v4 import Model as DeepseekV4Model
|
||||
from mlx_lm.models.deepseek_v32 import DeepseekV32MLP
|
||||
from mlx_lm.models.deepseek_v32 import Model as DeepseekV32Model
|
||||
from mlx_lm.models.gemma4 import Model as Gemma4Model
|
||||
@@ -295,6 +297,7 @@ def pipeline_auto_parallel(
|
||||
total = len(layers)
|
||||
for i, layer in enumerate(layers):
|
||||
mx.eval(layer) # type: ignore
|
||||
mx.clear_cache()
|
||||
yield ModelLoadingResponse(layers_loaded=i, total=total)
|
||||
|
||||
layers[0] = PipelineFirstLayer(layers[0], device_rank, group=group)
|
||||
@@ -510,6 +513,14 @@ def tensor_auto_parallel(
|
||||
all_to_sharded_linear_in_place,
|
||||
sharded_to_all_linear_in_place,
|
||||
)
|
||||
elif isinstance(model, DeepseekV4Model):
|
||||
tensor_parallel_sharding_strategy = DeepseekV4ShardingStrategy(
|
||||
group,
|
||||
all_to_sharded_linear,
|
||||
sharded_to_all_linear,
|
||||
all_to_sharded_linear_in_place,
|
||||
sharded_to_all_linear_in_place,
|
||||
)
|
||||
elif isinstance(model, MiniMaxModel):
|
||||
tensor_parallel_sharding_strategy = MiniMaxShardingStrategy(
|
||||
group,
|
||||
@@ -648,7 +659,14 @@ def _set_layers(model: nn.Module, layers: list[_LayerCallable]) -> None:
|
||||
|
||||
# Update DeepSeek V3 specific parameters when layers are shrunk
|
||||
if isinstance(
|
||||
model, (DeepseekV3Model, DeepseekV32Model, Glm4MoeModel, KimiK25Model)
|
||||
model,
|
||||
(
|
||||
DeepseekV3Model,
|
||||
DeepseekV32Model,
|
||||
DeepseekV4Model,
|
||||
Glm4MoeModel,
|
||||
KimiK25Model,
|
||||
),
|
||||
) and hasattr(inner_model_instance, "num_layers"):
|
||||
logger.info(
|
||||
f"Setting num_layers to {len(layers)} for model {model.model.__class__.__name__}"
|
||||
@@ -749,6 +767,178 @@ class ShardedMoE(CustomMlxLayer):
|
||||
return y
|
||||
|
||||
|
||||
class ShardedMoEV4(CustomMlxLayer):
|
||||
"""Same as ShardedMoE but for DeepseekV4MoE which takes (x, input_ids)."""
|
||||
|
||||
def __init__(self, layer: DeepseekV4MoE):
|
||||
super().__init__(cast(_LayerCallable, cast(object, layer)))
|
||||
self._v4_inner = layer
|
||||
self.sharding_group: mx.distributed.Group | None = None
|
||||
|
||||
def __call__(self, x: mx.array, input_ids: mx.array) -> mx.array:
|
||||
if self.sharding_group is not None:
|
||||
x = sum_gradients(self.sharding_group)(x)
|
||||
y = self._v4_inner(x, input_ids)
|
||||
if self.sharding_group is not None:
|
||||
y = mx.distributed.all_sum(y, group=self.sharding_group)
|
||||
return y
|
||||
|
||||
|
||||
def _shard_quantized_rows(
|
||||
q: nn.QuantizedLinear,
|
||||
head_dim: int,
|
||||
slicer: Callable[[mx.array, int], mx.array],
|
||||
) -> None:
|
||||
weight = q["weight"]
|
||||
scales = q["scales"]
|
||||
assert isinstance(weight, mx.array)
|
||||
assert isinstance(scales, mx.array)
|
||||
q.weight = slicer(weight, head_dim)
|
||||
q.scales = slicer(scales, head_dim)
|
||||
biases = q.get("biases")
|
||||
if isinstance(biases, mx.array):
|
||||
q.biases = slicer(biases, head_dim)
|
||||
|
||||
|
||||
class _AllSumLinear(nn.Module):
|
||||
"""Wraps an unsharded wo_b that takes a head-sharded partial wo_a output.
|
||||
|
||||
Flow per rank:
|
||||
1. all_sum the incoming partial wo_a output (summed across the head
|
||||
input shards → full wo_a_out on every rank)
|
||||
2. apply the unsharded wo_b → full hidden on every rank
|
||||
|
||||
One collective per layer on the smaller of (n_groups * o_lora_rank) vs
|
||||
hidden. wo_b compute is replicated, but at decode B=1 it's only ~30M FLOPs
|
||||
per layer and 61 extra all_gathers/token cost more than running wo_b on
|
||||
every rank.
|
||||
"""
|
||||
|
||||
def __init__(self, inner: nn.Module, group: mx.distributed.Group):
|
||||
super().__init__()
|
||||
self.inner = inner
|
||||
self._group = group
|
||||
|
||||
def __call__(self, x: mx.array) -> mx.array:
|
||||
x = mx.distributed.all_sum(x, group=self._group)
|
||||
return cast(Callable[[mx.array], mx.array], self.inner)(x)
|
||||
|
||||
|
||||
def _shard_v4_attention_heads(
|
||||
attn: V4Attention,
|
||||
world_size: int,
|
||||
rank: int,
|
||||
) -> None:
|
||||
"""Interleaved-per-group head sharding for V4Attention.
|
||||
|
||||
V4 uses a grouped low-rank output projection: `_grouped_output_projection`
|
||||
reshapes the flat `n_heads * head_dim` dim into `(o_groups, heads_per_group,
|
||||
head_dim)`, so group g owns heads `[g * heads_per_group : (g+1) * heads_per_group]`.
|
||||
|
||||
A naive contiguous `shard_linear("all-to-sharded")` on wq_b puts whole
|
||||
original groups on each rank — the per-rank "group g" ends up containing
|
||||
heads that don't belong to original group g. That breaks the wo_a grouped
|
||||
weight mapping. We instead slice heads interleaved-by-group: each rank
|
||||
owns `heads_per_group / N` heads *from every original group*, kept in
|
||||
group-major order so SDPA → reshape → wo_a preserves the group mapping.
|
||||
|
||||
Affects `wq_b.weight` / `wq_b.bias`, `attn_sink`. wo_a is sharded via a
|
||||
normal input-dim block split (the default axis-(-1) behavior of
|
||||
shard_inplace), which now correctly aligns with the interleaved head
|
||||
layout because the last dim of out after reshape is `heads_per_group/N *
|
||||
head_dim` per group.
|
||||
"""
|
||||
n_heads: int = attn.n_heads
|
||||
head_dim: int = attn.head_dim
|
||||
o_groups: int = attn.n_groups
|
||||
assert n_heads % o_groups == 0, "n_heads must be divisible by o_groups"
|
||||
heads_per_group = n_heads // o_groups
|
||||
assert heads_per_group % world_size == 0, (
|
||||
f"heads_per_group ({heads_per_group}) must be divisible by world_size "
|
||||
f"({world_size}) for interleaved per-group head sharding"
|
||||
)
|
||||
hpg_per_rank = heads_per_group // world_size
|
||||
start = rank * hpg_per_rank
|
||||
end = start + hpg_per_rank
|
||||
|
||||
def _slice_head_major_flat(arr: mx.array, stride: int) -> mx.array:
|
||||
"""Slice arr on axis 0 where the flat 0-axis is (o_groups *
|
||||
heads_per_group * stride), returning a fresh contiguous allocation
|
||||
so the full unsharded array can be freed. Without the contiguous
|
||||
copy the slice is a view and the original weight stays resident —
|
||||
OOM on large V4. Quantized packed weights don't round-trip through
|
||||
numpy so we use mx.contiguous directly."""
|
||||
rest = arr.shape[1:]
|
||||
reshaped = arr.reshape(o_groups, heads_per_group, stride, *rest)
|
||||
sliced = reshaped[:, start:end].reshape(o_groups * hpg_per_rank * stride, *rest)
|
||||
detached = mx.contiguous(sliced)
|
||||
mx.eval(detached)
|
||||
return detached
|
||||
|
||||
wq_b: nn.Module = attn.wq_b
|
||||
if isinstance(wq_b, nn.QuantizedLinear):
|
||||
# Packed weight: (n_heads*head_dim, q_lora_rank/el_per_int).
|
||||
# scales/biases: (n_heads*head_dim, q_lora_rank/group_size).
|
||||
# Slice axis 0 interleaved-by-group with head_dim stride.
|
||||
_shard_quantized_rows(wq_b, head_dim, _slice_head_major_flat)
|
||||
else:
|
||||
dense = wq_b
|
||||
assert isinstance(dense, nn.Linear)
|
||||
w = dense.weight
|
||||
q_lora_rank = w.shape[-1]
|
||||
w_sharded = _slice_head_major_flat(w, head_dim)
|
||||
has_bias = "bias" in dense
|
||||
new_wq_b = nn.Linear(q_lora_rank, w_sharded.shape[0], bias=has_bias)
|
||||
new_wq_b.weight = w_sharded
|
||||
if has_bias:
|
||||
b = dense.bias
|
||||
assert b is not None
|
||||
new_wq_b.bias = _slice_head_major_flat(b[:, None], head_dim).reshape(-1)
|
||||
attn.wq_b = new_wq_b
|
||||
|
||||
sink = attn.attn_sink
|
||||
reshaped = sink.reshape(o_groups, heads_per_group)[:, start:end].reshape(-1)
|
||||
detached_sink = mx.contiguous(reshaped)
|
||||
mx.eval(detached_sink)
|
||||
attn.attn_sink = detached_sink
|
||||
attn.n_heads = o_groups * hpg_per_rank
|
||||
|
||||
|
||||
class DeepseekV4ShardingStrategy(TensorParallelShardingStrategy):
|
||||
def shard_model(
|
||||
self,
|
||||
model: nn.Module,
|
||||
) -> Generator[ModelLoadingResponse, None, nn.Module]:
|
||||
model = cast(DeepseekV4Model, model)
|
||||
total = len(model.layers)
|
||||
|
||||
for i, layer in enumerate(model.layers):
|
||||
mx.eval(layer.parameters())
|
||||
|
||||
# Head-parallel attention with interleaved-per-group sharding.
|
||||
_shard_v4_attention_heads(layer.attn, self.N, self.group.rank())
|
||||
self.sharded_to_all_linear_in_place(layer.attn.wo_a)
|
||||
layer.attn.wo_b = _AllSumLinear(layer.attn.wo_b, self.group) # type: ignore[assignment]
|
||||
|
||||
ffn = layer.ffn
|
||||
if getattr(ffn, "shared_experts", None) is not None:
|
||||
self.all_to_sharded_linear_in_place(ffn.shared_experts.gate_proj)
|
||||
self.sharded_to_all_linear_in_place(ffn.shared_experts.down_proj)
|
||||
self.all_to_sharded_linear_in_place(ffn.shared_experts.up_proj)
|
||||
self.all_to_sharded_linear_in_place(ffn.switch_mlp.gate_proj)
|
||||
self.sharded_to_all_linear_in_place(ffn.switch_mlp.down_proj)
|
||||
self.all_to_sharded_linear_in_place(ffn.switch_mlp.up_proj)
|
||||
wrapped = ShardedMoEV4(ffn)
|
||||
wrapped.sharding_group = self.group
|
||||
layer.ffn = wrapped # type: ignore[assignment]
|
||||
|
||||
mx.eval(layer)
|
||||
mx.clear_cache()
|
||||
yield ModelLoadingResponse(layers_loaded=i, total=total)
|
||||
|
||||
return model
|
||||
|
||||
|
||||
class GLM4MoeLiteShardingStrategy(TensorParallelShardingStrategy):
|
||||
def shard_model(
|
||||
self,
|
||||
@@ -804,6 +994,7 @@ class GLM4MoeLiteShardingStrategy(TensorParallelShardingStrategy):
|
||||
layer.mlp = ShardedMoE(layer.mlp) # type: ignore
|
||||
layer.mlp.sharding_group = self.group # type: ignore
|
||||
mx.eval(layer)
|
||||
mx.clear_cache()
|
||||
|
||||
yield ModelLoadingResponse(layers_loaded=i, total=total)
|
||||
|
||||
@@ -921,6 +1112,7 @@ class MiniMaxShardingStrategy(TensorParallelShardingStrategy):
|
||||
layer.block_sparse_moe = ShardedMoE(layer.block_sparse_moe) # type: ignore
|
||||
layer.block_sparse_moe.sharding_group = self.group
|
||||
mx.eval(layer)
|
||||
mx.clear_cache()
|
||||
|
||||
yield ModelLoadingResponse(layers_loaded=i, total=total)
|
||||
return model
|
||||
@@ -1085,6 +1277,7 @@ class QwenShardingStrategy(TensorParallelShardingStrategy):
|
||||
layer.mlp.up_proj = self.all_to_sharded_linear(layer.mlp.up_proj)
|
||||
|
||||
mx.eval(layer)
|
||||
mx.clear_cache()
|
||||
|
||||
yield ModelLoadingResponse(layers_loaded=i, total=total)
|
||||
return model
|
||||
@@ -1130,6 +1323,7 @@ class Glm4MoeShardingStrategy(TensorParallelShardingStrategy):
|
||||
layer.mlp.up_proj = self.all_to_sharded_linear(layer.mlp.up_proj)
|
||||
|
||||
mx.eval(layer)
|
||||
mx.clear_cache()
|
||||
|
||||
yield ModelLoadingResponse(layers_loaded=i, total=total)
|
||||
return model
|
||||
@@ -1170,6 +1364,7 @@ class GptOssShardingStrategy(TensorParallelShardingStrategy):
|
||||
layer.mlp = ShardedMoE(layer.mlp) # type: ignore
|
||||
layer.mlp.sharding_group = self.group
|
||||
mx.eval(layer)
|
||||
mx.clear_cache()
|
||||
|
||||
yield ModelLoadingResponse(layers_loaded=i, total=total)
|
||||
return model
|
||||
@@ -1212,6 +1407,7 @@ class Step35ShardingStrategy(TensorParallelShardingStrategy):
|
||||
self.sharded_to_all_linear_in_place(layer.mlp.switch_mlp.down_proj)
|
||||
|
||||
mx.eval(layer)
|
||||
mx.clear_cache()
|
||||
|
||||
yield ModelLoadingResponse(layers_loaded=i, total=total)
|
||||
return model
|
||||
@@ -1254,6 +1450,7 @@ class NemotronHShardingStrategy(TensorParallelShardingStrategy):
|
||||
layer.mixer = mixer # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
mx.eval(layer)
|
||||
mx.clear_cache()
|
||||
yield ModelLoadingResponse(layers_loaded=i, total=total)
|
||||
return model
|
||||
|
||||
@@ -1391,5 +1588,6 @@ class Gemma4ShardingStrategy(TensorParallelShardingStrategy):
|
||||
layer.experts.sharding_group = self.group
|
||||
|
||||
mx.eval(layer)
|
||||
mx.clear_cache()
|
||||
yield ModelLoadingResponse(layers_loaded=i, total=total)
|
||||
return model
|
||||
@@ -46,7 +46,9 @@ class CacheSnapshot:
|
||||
"""Snapshot of states at a known token position."""
|
||||
|
||||
def __init__(
|
||||
self, states: list[RotatingKVCache | ArraysCache | None], token_count: int
|
||||
self,
|
||||
states: list[RotatingKVCache | ArraysCache | CacheList | None],
|
||||
token_count: int,
|
||||
):
|
||||
self.states = states
|
||||
self.token_count = token_count
|
||||
@@ -83,13 +85,55 @@ def copy_rotating_kv_cache(cache: RotatingKVCache) -> RotatingKVCache | None:
|
||||
return snap
|
||||
|
||||
|
||||
def _copy_arrays_cache(ac: ArraysCache) -> ArraysCache:
|
||||
entries: list[mx.array | None] = []
|
||||
for entry in ac.cache: # type: ignore[reportUnknownMemberType]
|
||||
if entry is None:
|
||||
entries.append(None)
|
||||
continue
|
||||
assert isinstance(entry, mx.array)
|
||||
entries.append(_detached_copy(entry))
|
||||
copy = ArraysCache(len(entries))
|
||||
copy.cache = entries # type: ignore[reportUnknownMemberType]
|
||||
return copy
|
||||
|
||||
|
||||
def _copy_cache_list(cl: CacheList) -> CacheList:
|
||||
inners: list[object] = list(cl) # type: ignore[reportUnknownArgumentType]
|
||||
copied: list[object] = []
|
||||
for inner in inners:
|
||||
if isinstance(inner, RotatingKVCache):
|
||||
snap = copy_rotating_kv_cache(inner)
|
||||
copied.append(snap if snap is not None else deepcopy(inner))
|
||||
elif isinstance(inner, ArraysCache):
|
||||
copied.append(_copy_arrays_cache(inner))
|
||||
else:
|
||||
copied.append(deepcopy(inner))
|
||||
return CacheList(*copied)
|
||||
|
||||
|
||||
def restore_snapshot_entry(
|
||||
entry: ArraysCache | RotatingKVCache | CacheList | None,
|
||||
) -> ArraysCache | RotatingKVCache | CacheList | None:
|
||||
if entry is None:
|
||||
return None
|
||||
if isinstance(entry, RotatingKVCache):
|
||||
snap = copy_rotating_kv_cache(entry)
|
||||
return snap if snap is not None else deepcopy(entry)
|
||||
if isinstance(entry, ArraysCache):
|
||||
return _copy_arrays_cache(entry)
|
||||
return _copy_cache_list(entry)
|
||||
|
||||
|
||||
def snapshot_ssm_states(cache: KVCacheType) -> CacheSnapshot:
|
||||
states: list[ArraysCache | RotatingKVCache | None] = []
|
||||
states: list[ArraysCache | RotatingKVCache | CacheList | None] = []
|
||||
for c in cache:
|
||||
if isinstance(c, ArraysCache):
|
||||
states.append(deepcopy(c))
|
||||
states.append(_copy_arrays_cache(c))
|
||||
elif isinstance(c, RotatingKVCache):
|
||||
states.append(copy_rotating_kv_cache(c))
|
||||
elif isinstance(c, CacheList) and not bool(c.is_trimmable()): # type: ignore[reportUnknownMemberType]
|
||||
states.append(_copy_cache_list(c))
|
||||
else:
|
||||
states.append(None)
|
||||
token_count = cache_length(cache)
|
||||
@@ -111,7 +155,12 @@ def _find_nearest_snapshot(
|
||||
|
||||
def has_non_kv_caches(cache: KVCacheType) -> bool:
|
||||
"""Check if a cache contains any ArraysCache (SSM) entries."""
|
||||
return any(isinstance(c, (ArraysCache, RotatingKVCache)) for c in cache)
|
||||
for c in cache:
|
||||
if isinstance(c, CacheList):
|
||||
return any(isinstance(_c, (ArraysCache, RotatingKVCache)) for _c in c) # type: ignore[reportUnknownVariableType]
|
||||
elif isinstance(c, (ArraysCache, RotatingKVCache)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class KVPrefixCache:
|
||||
@@ -357,11 +406,21 @@ def trim_cache(
|
||||
snapshot: CacheSnapshot | None = None,
|
||||
) -> None:
|
||||
for i, c in enumerate(cache):
|
||||
if isinstance(c, (ArraysCache, RotatingKVCache)):
|
||||
non_trimmable = isinstance(c, (ArraysCache, RotatingKVCache)) or (
|
||||
isinstance(c, CacheList) and not bool(c.is_trimmable()) # type: ignore[reportUnknownMemberType]
|
||||
)
|
||||
if non_trimmable:
|
||||
if snapshot is not None and snapshot.states[i] is not None:
|
||||
cache[i] = deepcopy(snapshot.states[i]) # type: ignore
|
||||
else:
|
||||
restored = restore_snapshot_entry(snapshot.states[i])
|
||||
if restored is not None:
|
||||
cache[i] = restored # type: ignore
|
||||
elif isinstance(c, (ArraysCache, RotatingKVCache)):
|
||||
c.state = [None] * len(c.state)
|
||||
else:
|
||||
# CacheList without a snapshot — zero each inner cache's state
|
||||
for inner in c: # type: ignore[reportUnknownVariableType]
|
||||
if isinstance(inner, (ArraysCache, RotatingKVCache)):
|
||||
inner.state = [None] * len(inner.state)
|
||||
else:
|
||||
c.trim(num_tokens)
|
||||
|
||||
|
||||
@@ -0,0 +1,837 @@
|
||||
# type: ignore
|
||||
"""
|
||||
DeepSeek-V4 Encoding
|
||||
|
||||
From upstream
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
# ============================================================
|
||||
# Special Tokens
|
||||
# ============================================================
|
||||
|
||||
bos_token: str = "<|begin▁of▁sentence|>"
|
||||
eos_token: str = "<|end▁of▁sentence|>"
|
||||
thinking_start_token: str = "<think>"
|
||||
thinking_end_token: str = "</think>"
|
||||
dsml_token: str = "|DSML|"
|
||||
|
||||
USER_SP_TOKEN = "<|User|>"
|
||||
ASSISTANT_SP_TOKEN = "<|Assistant|>"
|
||||
LATEST_REMINDER_SP_TOKEN = "<|latest_reminder|>"
|
||||
|
||||
# Task special tokens for internal classification tasks
|
||||
DS_TASK_SP_TOKENS = {
|
||||
"action": "<|action|>",
|
||||
"query": "<|query|>",
|
||||
"authority": "<|authority|>",
|
||||
"domain": "<|domain|>",
|
||||
"title": "<|title|>",
|
||||
"read_url": "<|read_url|>",
|
||||
}
|
||||
VALID_TASKS = set(DS_TASK_SP_TOKENS.keys())
|
||||
|
||||
# ============================================================
|
||||
# Templates
|
||||
# ============================================================
|
||||
|
||||
system_msg_template: str = "{content}"
|
||||
user_msg_template: str = "{content}"
|
||||
latest_reminder_msg_template: str = "{content}"
|
||||
assistant_msg_template: str = "{reasoning}{content}{tool_calls}" + eos_token
|
||||
assistant_msg_wo_eos_template: str = "{reasoning}{content}{tool_calls}"
|
||||
thinking_template: str = "{reasoning_content}"
|
||||
|
||||
response_format_template: str = "## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n{schema}"
|
||||
tool_call_template: str = (
|
||||
'<{dsml_token}invoke name="{name}">\n{arguments}\n</{dsml_token}invoke>'
|
||||
)
|
||||
tool_calls_template = (
|
||||
"<{dsml_token}{tc_block_name}>\n{tool_calls}\n</{dsml_token}{tc_block_name}>"
|
||||
)
|
||||
tool_calls_block_name: str = "tool_calls"
|
||||
|
||||
tool_output_template: str = "<tool_result>{content}</tool_result>"
|
||||
|
||||
REASONING_EFFORT_MAX = (
|
||||
"Reasoning Effort: Absolute maximum with no shortcuts permitted.\n"
|
||||
"You MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\n"
|
||||
"Explicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n"
|
||||
)
|
||||
|
||||
TOOLS_TEMPLATE = """## Tools
|
||||
|
||||
You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<{dsml_token}tool_calls>" block like the following:
|
||||
|
||||
<{dsml_token}tool_calls>
|
||||
<{dsml_token}invoke name="$TOOL_NAME">
|
||||
<{dsml_token}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</{dsml_token}parameter>
|
||||
...
|
||||
</{dsml_token}invoke>
|
||||
<{dsml_token}invoke name="$TOOL_NAME2">
|
||||
...
|
||||
</{dsml_token}invoke>
|
||||
</{dsml_token}tool_calls>
|
||||
|
||||
String parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.
|
||||
|
||||
If thinking_mode is enabled (triggered by {thinking_start_token}), you MUST output your complete reasoning inside {thinking_start_token}...{thinking_end_token} BEFORE any tool calls or final response.
|
||||
|
||||
Otherwise, output directly after {thinking_end_token} with tool calls or final response.
|
||||
|
||||
### Available Tool Schemas
|
||||
|
||||
{tool_schemas}
|
||||
|
||||
You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
|
||||
"""
|
||||
|
||||
# ============================================================
|
||||
# Utility Functions
|
||||
# ============================================================
|
||||
|
||||
|
||||
def to_json(value: Any) -> str:
|
||||
"""Serialize a value to JSON string."""
|
||||
try:
|
||||
return json.dumps(value, ensure_ascii=False)
|
||||
except: # noqa: E722
|
||||
return json.dumps(value, ensure_ascii=True)
|
||||
|
||||
|
||||
def tools_from_openai_format(tools):
|
||||
"""Extract function definitions from OpenAI-format tool list."""
|
||||
return [tool["function"] for tool in tools]
|
||||
|
||||
|
||||
def tool_calls_from_openai_format(tool_calls):
|
||||
"""Convert OpenAI-format tool calls to internal format."""
|
||||
return [
|
||||
{
|
||||
"name": tool_call["function"]["name"],
|
||||
"arguments": tool_call["function"]["arguments"],
|
||||
}
|
||||
for tool_call in tool_calls
|
||||
]
|
||||
|
||||
|
||||
def tool_calls_to_openai_format(tool_calls):
|
||||
"""Convert internal tool calls to OpenAI format."""
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_call["name"],
|
||||
"arguments": tool_call["arguments"],
|
||||
},
|
||||
}
|
||||
for tool_call in tool_calls
|
||||
]
|
||||
|
||||
|
||||
def encode_arguments_to_dsml(tool_call: Dict[str, str]) -> str:
|
||||
"""
|
||||
Encode tool call arguments into DSML parameter format.
|
||||
|
||||
Args:
|
||||
tool_call: Dict with "name" and "arguments" (JSON string) keys.
|
||||
|
||||
Returns:
|
||||
DSML-formatted parameter string.
|
||||
"""
|
||||
p_dsml_template = '<{dsml_token}parameter name="{key}" string="{is_str}">{value}</{dsml_token}parameter>'
|
||||
P_dsml_strs = [] # noqa: N806
|
||||
|
||||
try:
|
||||
arguments = json.loads(tool_call["arguments"])
|
||||
except Exception:
|
||||
arguments = {"arguments": tool_call["arguments"]}
|
||||
|
||||
for k, v in arguments.items():
|
||||
p_dsml_str = p_dsml_template.format(
|
||||
dsml_token=dsml_token,
|
||||
key=k,
|
||||
is_str="true" if isinstance(v, str) else "false",
|
||||
value=v if isinstance(v, str) else to_json(v),
|
||||
)
|
||||
P_dsml_strs.append(p_dsml_str)
|
||||
|
||||
return "\n".join(P_dsml_strs)
|
||||
|
||||
|
||||
def decode_dsml_to_arguments(
|
||||
tool_name: str, tool_args: Dict[str, Tuple[str, str]]
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Decode DSML parameters back to a tool call dict.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool.
|
||||
tool_args: Dict mapping param_name -> (value, is_string_flag).
|
||||
|
||||
Returns:
|
||||
Dict with "name" and "arguments" (JSON string) keys.
|
||||
"""
|
||||
|
||||
def _decode_value(key: str, value: str, string: str):
|
||||
if string == "true":
|
||||
value = to_json(value)
|
||||
return f"{to_json(key)}: {value}"
|
||||
|
||||
tool_args_json = (
|
||||
"{"
|
||||
+ ", ".join(
|
||||
[_decode_value(k, v, string=is_str) for k, (v, is_str) in tool_args.items()]
|
||||
)
|
||||
+ "}"
|
||||
)
|
||||
return dict(name=tool_name, arguments=tool_args_json)
|
||||
|
||||
|
||||
def render_tools(tools: List[Dict[str, Union[str, Dict[str, Any]]]]) -> str:
|
||||
"""
|
||||
Render tool schemas into the system prompt format.
|
||||
|
||||
Args:
|
||||
tools: List of tool schema dicts (each with name, description, parameters).
|
||||
|
||||
Returns:
|
||||
Formatted tools section string.
|
||||
"""
|
||||
tools_json = [to_json(t) for t in tools]
|
||||
|
||||
return TOOLS_TEMPLATE.format(
|
||||
tool_schemas="\n".join(tools_json),
|
||||
dsml_token=dsml_token,
|
||||
thinking_start_token=thinking_start_token,
|
||||
thinking_end_token=thinking_end_token,
|
||||
)
|
||||
|
||||
|
||||
def find_last_user_index(messages: List[Dict[str, Any]]) -> int:
|
||||
"""Find the index of the last user/developer message."""
|
||||
last_user_index = -1
|
||||
for idx in range(len(messages) - 1, -1, -1):
|
||||
if messages[idx].get("role") in ["user", "developer"]:
|
||||
last_user_index = idx
|
||||
break
|
||||
return last_user_index
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Message Rendering
|
||||
# ============================================================
|
||||
|
||||
|
||||
def render_message(
|
||||
index: int,
|
||||
messages: List[Dict[str, Any]],
|
||||
thinking_mode: str,
|
||||
drop_thinking: bool = True,
|
||||
reasoning_effort: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Render a single message at the given index into its encoded string form.
|
||||
|
||||
This is the core function that converts each message in the conversation
|
||||
into the DeepSeek-V4 format.
|
||||
|
||||
Args:
|
||||
index: Index of the message to render.
|
||||
messages: Full list of messages in the conversation.
|
||||
thinking_mode: Either "chat" or "thinking".
|
||||
drop_thinking: Whether to drop reasoning content from earlier turns.
|
||||
reasoning_effort: Optional reasoning effort level ("max", "high", or None).
|
||||
|
||||
Returns:
|
||||
Encoded string for this message.
|
||||
"""
|
||||
assert 0 <= index < len(messages)
|
||||
assert thinking_mode in ["chat", "thinking"], (
|
||||
f"Invalid thinking_mode `{thinking_mode}`"
|
||||
)
|
||||
|
||||
prompt = ""
|
||||
msg = messages[index]
|
||||
last_user_idx = find_last_user_index(messages)
|
||||
|
||||
role = msg.get("role")
|
||||
content = msg.get("content")
|
||||
tools = msg.get("tools")
|
||||
response_format = msg.get("response_format")
|
||||
tool_calls = msg.get("tool_calls")
|
||||
reasoning_content = msg.get("reasoning_content")
|
||||
wo_eos = msg.get("wo_eos", False)
|
||||
|
||||
if tools:
|
||||
tools = tools_from_openai_format(tools)
|
||||
if tool_calls:
|
||||
tool_calls = tool_calls_from_openai_format(tool_calls)
|
||||
|
||||
# Reasoning effort prefix (only at index 0 in thinking mode with max effort)
|
||||
assert reasoning_effort in ["max", None, "high"], (
|
||||
f"Invalid reasoning effort: {reasoning_effort}"
|
||||
)
|
||||
if index == 0 and thinking_mode == "thinking" and reasoning_effort == "max":
|
||||
prompt += REASONING_EFFORT_MAX
|
||||
|
||||
if role == "system":
|
||||
prompt += system_msg_template.format(content=content or "")
|
||||
if tools:
|
||||
prompt += "\n\n" + render_tools(tools)
|
||||
if response_format:
|
||||
prompt += "\n\n" + response_format_template.format(
|
||||
schema=to_json(response_format)
|
||||
)
|
||||
|
||||
elif role == "developer":
|
||||
assert content, f"Invalid message for role `{role}`: {msg}"
|
||||
|
||||
content_developer = USER_SP_TOKEN
|
||||
content_developer += content
|
||||
|
||||
if tools:
|
||||
content_developer += "\n\n" + render_tools(tools)
|
||||
if response_format:
|
||||
content_developer += "\n\n" + response_format_template.format(
|
||||
schema=to_json(response_format)
|
||||
)
|
||||
|
||||
prompt += user_msg_template.format(content=content_developer)
|
||||
|
||||
elif role == "user":
|
||||
prompt += USER_SP_TOKEN
|
||||
|
||||
# Handle content blocks (tool results mixed with text)
|
||||
content_blocks = msg.get("content_blocks")
|
||||
if content_blocks:
|
||||
parts = []
|
||||
for block in content_blocks:
|
||||
block_type = block.get("type")
|
||||
if block_type == "text":
|
||||
parts.append(block.get("text", ""))
|
||||
elif block_type == "tool_result":
|
||||
tool_content = block.get("content", "")
|
||||
if isinstance(tool_content, list):
|
||||
text_parts = []
|
||||
for b in tool_content:
|
||||
if b.get("type") == "text":
|
||||
text_parts.append(b.get("text", ""))
|
||||
else:
|
||||
text_parts.append(f"[Unsupported {b.get('type')}]")
|
||||
tool_content = "\n\n".join(text_parts)
|
||||
parts.append(tool_output_template.format(content=tool_content))
|
||||
else:
|
||||
parts.append(f"[Unsupported {block_type}]")
|
||||
prompt += "\n\n".join(parts)
|
||||
else:
|
||||
prompt += content or ""
|
||||
|
||||
elif role == "latest_reminder":
|
||||
prompt += LATEST_REMINDER_SP_TOKEN + latest_reminder_msg_template.format(
|
||||
content=content
|
||||
)
|
||||
|
||||
elif role == "tool":
|
||||
raise NotImplementedError(
|
||||
"deepseek_v4 merges tool messages into user; please preprocess with merge_tool_messages()"
|
||||
)
|
||||
|
||||
elif role == "assistant":
|
||||
thinking_part = ""
|
||||
tc_content = ""
|
||||
|
||||
if tool_calls:
|
||||
tc_list = [
|
||||
tool_call_template.format(
|
||||
dsml_token=dsml_token,
|
||||
name=tc.get("name"),
|
||||
arguments=encode_arguments_to_dsml(tc),
|
||||
)
|
||||
for tc in tool_calls
|
||||
]
|
||||
tc_content += "\n\n" + tool_calls_template.format(
|
||||
dsml_token=dsml_token,
|
||||
tool_calls="\n".join(tc_list),
|
||||
tc_block_name=tool_calls_block_name,
|
||||
)
|
||||
|
||||
summary_content = content or ""
|
||||
rc = reasoning_content or ""
|
||||
|
||||
# Check if previous message has a task - if so, this is a task output (no thinking)
|
||||
prev_has_task = index - 1 >= 0 and messages[index - 1].get("task") is not None
|
||||
|
||||
if thinking_mode == "thinking" and not prev_has_task:
|
||||
if not drop_thinking or index > last_user_idx:
|
||||
thinking_part = (
|
||||
thinking_template.format(reasoning_content=rc) + thinking_end_token
|
||||
)
|
||||
else:
|
||||
thinking_part = ""
|
||||
|
||||
if wo_eos:
|
||||
prompt += assistant_msg_wo_eos_template.format(
|
||||
reasoning=thinking_part,
|
||||
content=summary_content,
|
||||
tool_calls=tc_content,
|
||||
)
|
||||
else:
|
||||
prompt += assistant_msg_template.format(
|
||||
reasoning=thinking_part,
|
||||
content=summary_content,
|
||||
tool_calls=tc_content,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown role: {role}")
|
||||
|
||||
# Append transition tokens based on what follows
|
||||
if index + 1 < len(messages) and messages[index + 1].get("role") not in [
|
||||
"assistant",
|
||||
"latest_reminder",
|
||||
]:
|
||||
return prompt
|
||||
|
||||
task = messages[index].get("task")
|
||||
if task is not None:
|
||||
# Task special token for internal classification tasks
|
||||
assert task in VALID_TASKS, (
|
||||
f"Invalid task: '{task}'. Valid tasks are: {list(VALID_TASKS)}"
|
||||
)
|
||||
task_sp_token = DS_TASK_SP_TOKENS[task]
|
||||
|
||||
if task != "action":
|
||||
# Non-action tasks: append task sp token directly after the message
|
||||
prompt += task_sp_token
|
||||
else:
|
||||
# Action task: append Assistant + thinking token + action sp token
|
||||
prompt += ASSISTANT_SP_TOKEN
|
||||
prompt += (
|
||||
thinking_end_token
|
||||
if thinking_mode != "thinking"
|
||||
else thinking_start_token
|
||||
)
|
||||
prompt += task_sp_token
|
||||
|
||||
elif messages[index].get("role") in ["user", "developer"]:
|
||||
# Normal generation: append Assistant + thinking token
|
||||
prompt += ASSISTANT_SP_TOKEN
|
||||
if (
|
||||
not drop_thinking
|
||||
and thinking_mode == "thinking"
|
||||
or drop_thinking
|
||||
and thinking_mode == "thinking"
|
||||
and index >= last_user_idx
|
||||
):
|
||||
prompt += thinking_start_token
|
||||
else:
|
||||
prompt += thinking_end_token
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Preprocessing
|
||||
# ============================================================
|
||||
|
||||
|
||||
def merge_tool_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Merge tool messages into the preceding user message using content_blocks format.
|
||||
|
||||
DeepSeek-V4 does not have a standalone "tool" role; instead, tool results
|
||||
are encoded as <tool_result> blocks within user messages.
|
||||
|
||||
This function converts a standard OpenAI-format conversation (with separate
|
||||
"tool" role messages) into V4 format where tool results are merged into
|
||||
user messages.
|
||||
|
||||
Args:
|
||||
messages: List of message dicts in OpenAI format.
|
||||
|
||||
Returns:
|
||||
Processed message list with tool messages merged into user messages.
|
||||
"""
|
||||
merged: List[Dict[str, Any]] = []
|
||||
|
||||
for msg in messages:
|
||||
msg = copy.deepcopy(msg)
|
||||
role = msg.get("role")
|
||||
|
||||
if role == "tool":
|
||||
# Convert tool message to a user message with tool_result block
|
||||
tool_block = {
|
||||
"type": "tool_result",
|
||||
"tool_use_id": msg.get("tool_call_id", ""),
|
||||
"content": msg.get("content", ""),
|
||||
}
|
||||
# Merge into previous message if it's already a user (merged tool)
|
||||
if (
|
||||
merged
|
||||
and merged[-1].get("role") == "user"
|
||||
and "content_blocks" in merged[-1]
|
||||
):
|
||||
merged[-1]["content_blocks"].append(tool_block)
|
||||
else:
|
||||
merged.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content_blocks": [tool_block],
|
||||
}
|
||||
)
|
||||
elif role == "user":
|
||||
text_block = {"type": "text", "text": msg.get("content", "")}
|
||||
if (
|
||||
merged
|
||||
and merged[-1].get("role") == "user"
|
||||
and "content_blocks" in merged[-1]
|
||||
and merged[-1].get("task") is None
|
||||
):
|
||||
merged[-1]["content_blocks"].append(text_block)
|
||||
else:
|
||||
new_msg = {
|
||||
"role": "user",
|
||||
"content": msg.get("content", ""),
|
||||
"content_blocks": [text_block],
|
||||
}
|
||||
# Preserve extra fields (task, wo_eos, mask, etc.)
|
||||
for key in ("task", "wo_eos", "mask"):
|
||||
if key in msg:
|
||||
new_msg[key] = msg[key]
|
||||
merged.append(new_msg)
|
||||
else:
|
||||
merged.append(msg)
|
||||
|
||||
return merged
|
||||
|
||||
|
||||
def sort_tool_results_by_call_order(
|
||||
messages: List[Dict[str, Any]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Sort tool_result blocks within user messages by the order of tool_calls
|
||||
in the preceding assistant message.
|
||||
|
||||
Args:
|
||||
messages: Preprocessed message list (after merge_tool_messages).
|
||||
|
||||
Returns:
|
||||
Message list with sorted tool result blocks.
|
||||
"""
|
||||
last_tool_call_order: Dict[str, int] = {}
|
||||
|
||||
for msg in messages:
|
||||
role = msg.get("role")
|
||||
if role == "assistant" and msg.get("tool_calls"):
|
||||
last_tool_call_order = {}
|
||||
for idx, tc in enumerate(msg["tool_calls"]):
|
||||
tc_id = tc.get("id") or tc.get("function", {}).get("id", "")
|
||||
if tc_id:
|
||||
last_tool_call_order[tc_id] = idx
|
||||
|
||||
elif role == "user" and msg.get("content_blocks"):
|
||||
tool_blocks = [
|
||||
b for b in msg["content_blocks"] if b.get("type") == "tool_result"
|
||||
]
|
||||
if len(tool_blocks) > 1 and last_tool_call_order:
|
||||
sorted_blocks = sorted(
|
||||
tool_blocks,
|
||||
key=lambda b: last_tool_call_order.get(b.get("tool_use_id", ""), 0),
|
||||
)
|
||||
sorted_idx = 0
|
||||
new_blocks = []
|
||||
for block in msg["content_blocks"]:
|
||||
if block.get("type") == "tool_result":
|
||||
new_blocks.append(sorted_blocks[sorted_idx])
|
||||
sorted_idx += 1
|
||||
else:
|
||||
new_blocks.append(block)
|
||||
msg["content_blocks"] = new_blocks
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Main Encoding Function
|
||||
# ============================================================
|
||||
|
||||
|
||||
def encode_messages(
|
||||
messages: List[Dict[str, Any]],
|
||||
thinking_mode: str,
|
||||
context: Optional[List[Dict[str, Any]]] = None,
|
||||
drop_thinking: bool = True,
|
||||
add_default_bos_token: bool = True,
|
||||
reasoning_effort: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Encode a list of messages into the DeepSeek-V4 prompt format.
|
||||
|
||||
This is the main entry point for encoding conversations. It handles:
|
||||
- BOS token insertion
|
||||
- Thinking mode with optional reasoning content dropping
|
||||
- Tool message merging into user messages
|
||||
- Multi-turn conversation context
|
||||
|
||||
Args:
|
||||
messages: List of message dicts to encode.
|
||||
thinking_mode: Either "chat" or "thinking".
|
||||
context: Optional preceding context messages (already encoded prefix).
|
||||
drop_thinking: If True, drop reasoning_content from earlier assistant turns
|
||||
(only keep reasoning for messages after the last user message).
|
||||
add_default_bos_token: Whether to prepend BOS token at conversation start.
|
||||
reasoning_effort: Optional reasoning effort level ("max", "high", or None).
|
||||
|
||||
Returns:
|
||||
The encoded prompt string.
|
||||
"""
|
||||
context = context if context else []
|
||||
|
||||
# Preprocess: merge tool messages and sort tool results
|
||||
messages = merge_tool_messages(messages)
|
||||
messages = sort_tool_results_by_call_order(context + messages)[len(context) :]
|
||||
if context:
|
||||
context = merge_tool_messages(context)
|
||||
context = sort_tool_results_by_call_order(context)
|
||||
|
||||
full_messages = context + messages
|
||||
|
||||
prompt = bos_token if add_default_bos_token and len(context) == 0 else ""
|
||||
|
||||
# Resolve drop_thinking: if any message has tools defined, don't drop thinking
|
||||
effective_drop_thinking = drop_thinking
|
||||
if any(m.get("tools") for m in full_messages):
|
||||
effective_drop_thinking = False
|
||||
|
||||
if thinking_mode == "thinking" and effective_drop_thinking:
|
||||
full_messages = _drop_thinking_messages(full_messages)
|
||||
# After dropping, recalculate how many messages to render
|
||||
# (context may have shrunk too)
|
||||
num_to_render = len(full_messages) - len(_drop_thinking_messages(context))
|
||||
context_len = len(full_messages) - num_to_render
|
||||
else:
|
||||
num_to_render = len(messages)
|
||||
context_len = len(context)
|
||||
|
||||
for idx in range(num_to_render):
|
||||
prompt += render_message(
|
||||
idx + context_len,
|
||||
full_messages,
|
||||
thinking_mode=thinking_mode,
|
||||
drop_thinking=effective_drop_thinking,
|
||||
reasoning_effort=reasoning_effort,
|
||||
)
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
def _drop_thinking_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Drop reasoning_content and non-essential messages before the last user message.
|
||||
|
||||
Behavior:
|
||||
- Messages with role in ["user", "system", "tool", "latest_reminder"] are always kept.
|
||||
- Messages at or after the last user index are always kept.
|
||||
- Assistant messages before the last user get reasoning_content removed.
|
||||
- Developer messages before the last user are dropped entirely.
|
||||
"""
|
||||
last_user_idx = find_last_user_index(messages)
|
||||
result = []
|
||||
keep_roles = {"user", "system", "tool", "latest_reminder", "direct_search_results"}
|
||||
|
||||
for idx, msg in enumerate(messages):
|
||||
role = msg.get("role")
|
||||
if role in keep_roles or idx >= last_user_idx:
|
||||
result.append(msg)
|
||||
elif role == "assistant":
|
||||
msg = copy.copy(msg)
|
||||
msg.pop("reasoning_content", None)
|
||||
result.append(msg)
|
||||
# developer and other roles before last_user_idx are dropped
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Parsing (Decoding model output)
|
||||
# ============================================================
|
||||
|
||||
|
||||
def _read_until_stop(
|
||||
index: int, text: str, stop: List[str]
|
||||
) -> Tuple[int, str, Optional[str]]:
|
||||
"""
|
||||
Read text from index until one of the stop strings is found.
|
||||
|
||||
Returns:
|
||||
Tuple of (new_index, content_before_stop, matched_stop_string_or_None).
|
||||
"""
|
||||
min_pos = len(text)
|
||||
matched_stop = None
|
||||
|
||||
for s in stop:
|
||||
pos = text.find(s, index)
|
||||
if pos != -1 and pos < min_pos:
|
||||
min_pos = pos
|
||||
matched_stop = s
|
||||
|
||||
if matched_stop:
|
||||
content = text[index:min_pos]
|
||||
return min_pos + len(matched_stop), content, matched_stop
|
||||
else:
|
||||
content = text[index:]
|
||||
return len(text), content, None
|
||||
|
||||
|
||||
def parse_tool_calls(
|
||||
index: int, text: str
|
||||
) -> Tuple[int, Optional[str], List[Dict[str, str]]]:
|
||||
"""
|
||||
Parse DSML tool calls from text starting at the given index.
|
||||
|
||||
Args:
|
||||
index: Starting position in text.
|
||||
text: The full text to parse.
|
||||
|
||||
Returns:
|
||||
Tuple of (new_index, last_stop_token, list_of_tool_call_dicts).
|
||||
Each tool call dict has "name" and "arguments" keys.
|
||||
"""
|
||||
tool_calls: List[Dict[str, Any]] = []
|
||||
stop_token = None
|
||||
tool_calls_end_token = f"</{dsml_token}{tool_calls_block_name}>"
|
||||
|
||||
while index < len(text):
|
||||
index, _, stop_token = _read_until_stop(
|
||||
index, text, [f"<{dsml_token}invoke", tool_calls_end_token]
|
||||
)
|
||||
if _ != ">\n":
|
||||
raise ValueError(f"Tool call format error: expected '>\\n' but got '{_}'")
|
||||
|
||||
if stop_token == tool_calls_end_token:
|
||||
break
|
||||
|
||||
if stop_token is None:
|
||||
raise ValueError("Missing special token in tool calls")
|
||||
|
||||
index, tool_name_content, stop_token = _read_until_stop(
|
||||
index, text, [f"<{dsml_token}parameter", f"</{dsml_token}invoke"]
|
||||
)
|
||||
|
||||
p_tool_name = re.findall(
|
||||
r'^\s*name="(.*?)">\n$', tool_name_content, flags=re.DOTALL
|
||||
)
|
||||
if len(p_tool_name) != 1:
|
||||
raise ValueError(f"Tool name format error: '{tool_name_content}'")
|
||||
tool_name = p_tool_name[0]
|
||||
|
||||
tool_args: Dict[str, Tuple[str, str]] = {}
|
||||
while stop_token == f"<{dsml_token}parameter":
|
||||
index, param_content, stop_token = _read_until_stop(
|
||||
index, text, [f"/{dsml_token}parameter"]
|
||||
)
|
||||
|
||||
param_kv = re.findall(
|
||||
r'^ name="(.*?)" string="(true|false)">(.*?)<$',
|
||||
param_content,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
if len(param_kv) != 1:
|
||||
raise ValueError(f"Parameter format error: '{param_content}'")
|
||||
param_name, string, param_value = param_kv[0]
|
||||
|
||||
if param_name in tool_args:
|
||||
raise ValueError(f"Duplicate parameter name: '{param_name}'")
|
||||
tool_args[param_name] = (param_value, string)
|
||||
|
||||
index, content, stop_token = _read_until_stop(
|
||||
index, text, [f"<{dsml_token}parameter", f"</{dsml_token}invoke"]
|
||||
)
|
||||
if content != ">\n":
|
||||
raise ValueError(
|
||||
f"Parameter format error: expected '>\\n' but got '{content}'"
|
||||
)
|
||||
|
||||
tool_call = decode_dsml_to_arguments(tool_name=tool_name, tool_args=tool_args)
|
||||
tool_calls.append(tool_call)
|
||||
|
||||
return index, stop_token, tool_calls
|
||||
|
||||
|
||||
def parse_message_from_completion_text(text: str, thinking_mode: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse a model completion text into a structured assistant message.
|
||||
|
||||
This function takes the raw text output from the model (a single assistant turn)
|
||||
and extracts:
|
||||
- reasoning_content (thinking block)
|
||||
- content (summary/response)
|
||||
- tool_calls (if any)
|
||||
|
||||
NOTE: This function is designed to parse only correctly formatted strings and
|
||||
will raise ValueError for malformed output.
|
||||
|
||||
Args:
|
||||
text: The raw completion text (including EOS token).
|
||||
thinking_mode: Either "chat" or "thinking".
|
||||
|
||||
Returns:
|
||||
Dict with keys: "role", "content", "reasoning_content", "tool_calls".
|
||||
tool_calls are in OpenAI format.
|
||||
"""
|
||||
summary_content, reasoning_content, tool_calls = "", "", []
|
||||
index, stop_token = 0, None
|
||||
tool_calls_start_token = f"\n\n<{dsml_token}{tool_calls_block_name}"
|
||||
|
||||
is_thinking = thinking_mode == "thinking"
|
||||
is_tool_calling = False
|
||||
|
||||
if is_thinking:
|
||||
index, content_delta, stop_token = _read_until_stop(
|
||||
index, text, [thinking_end_token, tool_calls_start_token]
|
||||
)
|
||||
reasoning_content = content_delta
|
||||
assert stop_token == thinking_end_token, (
|
||||
"Invalid thinking format: missing </think>"
|
||||
)
|
||||
|
||||
index, content_delta, stop_token = _read_until_stop(
|
||||
index, text, [eos_token, tool_calls_start_token]
|
||||
)
|
||||
summary_content = content_delta
|
||||
if stop_token == tool_calls_start_token:
|
||||
is_tool_calling = True
|
||||
else:
|
||||
assert stop_token == eos_token, "Invalid format: missing EOS token"
|
||||
|
||||
if is_tool_calling:
|
||||
index, stop_token, tool_calls = parse_tool_calls(index, text)
|
||||
|
||||
index, tool_ends_text, stop_token = _read_until_stop(index, text, [eos_token])
|
||||
assert not tool_ends_text, "Unexpected content after tool calls"
|
||||
|
||||
assert len(text) == index and stop_token in [eos_token, None], (
|
||||
"Unexpected content at end"
|
||||
)
|
||||
|
||||
for sp_token in [
|
||||
bos_token,
|
||||
eos_token,
|
||||
thinking_start_token,
|
||||
thinking_end_token,
|
||||
dsml_token,
|
||||
]:
|
||||
assert sp_token not in summary_content and sp_token not in reasoning_content, (
|
||||
f"Unexpected special token '{sp_token}' in content"
|
||||
)
|
||||
|
||||
return {
|
||||
"role": "assistant",
|
||||
"content": summary_content,
|
||||
"reasoning_content": reasoning_content,
|
||||
"tool_calls": tool_calls_to_openai_format(tool_calls),
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import contextlib
|
||||
import functools
|
||||
import math
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from typing import Callable, Generator, cast, get_args
|
||||
|
||||
import mlx.core as mx
|
||||
@@ -10,7 +9,7 @@ from mlx_lm.generate import (
|
||||
maybe_quantize_kv_cache,
|
||||
stream_generate,
|
||||
)
|
||||
from mlx_lm.models.cache import ArraysCache, RotatingKVCache
|
||||
from mlx_lm.models.cache import ArraysCache, CacheList, RotatingKVCache
|
||||
from mlx_lm.sample_utils import make_logits_processors, make_sampler
|
||||
from mlx_lm.tokenizer_utils import TokenizerWrapper
|
||||
|
||||
@@ -47,6 +46,7 @@ from exo.worker.engines.mlx.cache import (
|
||||
encode_prompt,
|
||||
has_non_kv_caches,
|
||||
make_kv_cache,
|
||||
restore_snapshot_entry,
|
||||
snapshot_ssm_states,
|
||||
)
|
||||
from exo.worker.engines.mlx.constants import (
|
||||
@@ -370,14 +370,18 @@ def prefill(
|
||||
|
||||
# stream_generate added 1 extra generated token to the cache, so we should trim it.
|
||||
# Because of needing to roll back arrays cache, we will generate on 2 tokens so trim 1 more.
|
||||
pre_gen = deepcopy(snapshots[-2]) if has_ssm else None
|
||||
pre_gen = snapshots[-2] if has_ssm else None
|
||||
for i, c in enumerate(cache):
|
||||
if has_ssm and isinstance(c, (ArraysCache, RotatingKVCache)):
|
||||
non_trimmable = isinstance(c, (ArraysCache, RotatingKVCache)) or (
|
||||
isinstance(c, CacheList) and not bool(c.is_trimmable()) # type: ignore[reportUnknownMemberType]
|
||||
)
|
||||
if has_ssm and non_trimmable:
|
||||
assert pre_gen is not None
|
||||
if pre_gen.states[i] is not None:
|
||||
cache[i] = deepcopy(pre_gen.states[i]) # type: ignore
|
||||
restored = restore_snapshot_entry(pre_gen.states[i])
|
||||
if restored is not None:
|
||||
cache[i] = restored # type: ignore
|
||||
else:
|
||||
assert not isinstance(c, (ArraysCache, RotatingKVCache))
|
||||
assert not non_trimmable
|
||||
c.trim(2)
|
||||
|
||||
elapsed = time.perf_counter() - start_time
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from exo.worker.engines.mlx.patches.opt_batch_gen import apply_batch_gen_patch
|
||||
from exo.worker.engines.mlx.patches.standard_yarn_rope import patch_yarn_rope
|
||||
from exo.worker.engines.mlx.patches.v4_offset_sync import apply as apply_v4_offset_sync
|
||||
|
||||
_applied = False
|
||||
|
||||
@@ -11,3 +12,4 @@ def apply_mlx_patches() -> None:
|
||||
_applied = True
|
||||
patch_yarn_rope()
|
||||
apply_batch_gen_patch()
|
||||
apply_v4_offset_sync()
|
||||
@@ -58,6 +58,7 @@ def _patched_step(self: GenerationBatch) -> tuple[list[int], list[mx.array]]:
|
||||
self._current_tokens = self._next_tokens
|
||||
self._current_logprobs = self._next_logprobs
|
||||
inputs = self._current_tokens
|
||||
assert inputs is not None, "_step requires initialized _next_tokens"
|
||||
|
||||
buf = _get_buffer(self)
|
||||
buf.ready = buf.pending
|
||||
@@ -87,7 +88,7 @@ def _patched_step(self: GenerationBatch) -> tuple[list[int], list[mx.array]]:
|
||||
sampled = self.fallback_sampler(logprobs)
|
||||
|
||||
self._next_tokens = sampled
|
||||
self._next_logprobs = list(logprobs)
|
||||
self._next_logprobs = logprobs
|
||||
|
||||
if buf.needs_topk:
|
||||
batch_size = len(self.uids)
|
||||
@@ -106,19 +107,29 @@ def _patched_step(self: GenerationBatch) -> tuple[list[int], list[mx.array]]:
|
||||
)
|
||||
mx.async_eval(
|
||||
self._next_tokens,
|
||||
*self._next_logprobs,
|
||||
self._next_logprobs,
|
||||
pending_indices,
|
||||
pending_values,
|
||||
pending_selected,
|
||||
)
|
||||
else:
|
||||
mx.async_eval(self._next_tokens, *self._next_logprobs)
|
||||
mx.async_eval(self._next_tokens, self._next_logprobs)
|
||||
|
||||
current_lp = self._current_logprobs
|
||||
if isinstance(current_lp, mx.array):
|
||||
mx.eval(inputs, current_lp)
|
||||
elif current_lp:
|
||||
mx.eval(inputs, *current_lp)
|
||||
else:
|
||||
mx.eval(inputs)
|
||||
|
||||
mx.eval(inputs, *self._current_logprobs)
|
||||
token_list = cast(list[int], inputs.tolist())
|
||||
for sti, ti in zip(self.tokens, token_list, strict=True):
|
||||
sti.append(ti)
|
||||
return token_list, self._current_logprobs
|
||||
|
||||
if isinstance(current_lp, mx.array):
|
||||
current_lp = list(current_lp)
|
||||
return token_list, current_lp
|
||||
|
||||
|
||||
def apply_batch_gen_patch() -> None:
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
from typing import Callable, cast
|
||||
|
||||
import mlx.core as mx
|
||||
from mlx_lm.models.deepseek_v4 import Compressor, DeepseekV4Model
|
||||
|
||||
_current_int_offset: int | None = None
|
||||
_applied: bool = False
|
||||
|
||||
|
||||
def _extract_int_offset(cache: object | None) -> int | None:
|
||||
if cache is None:
|
||||
return None
|
||||
for entry in cast(list[object], cache):
|
||||
inner_caches = getattr(entry, "caches", None)
|
||||
win = inner_caches[0] if inner_caches is not None else entry
|
||||
int_off = getattr(win, "_offset", None)
|
||||
if isinstance(int_off, int):
|
||||
return int_off
|
||||
maybe_int = getattr(win, "offset", None)
|
||||
if isinstance(maybe_int, int):
|
||||
return maybe_int
|
||||
return None
|
||||
|
||||
|
||||
_ModelCall = Callable[[DeepseekV4Model, mx.array, list[object] | None], mx.array]
|
||||
_CompressorCall = Callable[
|
||||
[Compressor, mx.array, object, object, int, int, int], mx.array | None
|
||||
]
|
||||
|
||||
|
||||
def apply() -> None:
|
||||
global _applied
|
||||
if _applied:
|
||||
return
|
||||
_applied = True
|
||||
|
||||
original_model_call = cast(_ModelCall, DeepseekV4Model.__call__)
|
||||
|
||||
def patched_model_call(
|
||||
self: DeepseekV4Model,
|
||||
inputs: mx.array,
|
||||
cache: list[object] | None = None,
|
||||
) -> mx.array:
|
||||
global _current_int_offset
|
||||
prev = _current_int_offset
|
||||
_current_int_offset = _extract_int_offset(cache)
|
||||
try:
|
||||
return original_model_call(self, inputs, cache)
|
||||
finally:
|
||||
_current_int_offset = prev
|
||||
|
||||
DeepseekV4Model.__call__ = patched_model_call
|
||||
|
||||
original_compressor_call = cast(_CompressorCall, Compressor.__call__)
|
||||
|
||||
def patched_compressor_call(
|
||||
self: Compressor,
|
||||
x: mx.array,
|
||||
state: object,
|
||||
offset: object,
|
||||
slot_compressed: int,
|
||||
slot_kv_state: int,
|
||||
slot_score_state: int,
|
||||
) -> mx.array | None:
|
||||
if isinstance(offset, mx.array) and _current_int_offset is not None:
|
||||
offset = _current_int_offset
|
||||
return original_compressor_call(
|
||||
self, x, state, offset, slot_compressed, slot_kv_state, slot_score_state
|
||||
)
|
||||
|
||||
Compressor.__call__ = patched_compressor_call
|
||||
|
||||
|
||||
apply()
|
||||
@@ -165,6 +165,8 @@ def load_mlx_items(
|
||||
) -> Generator[
|
||||
ModelLoadingResponse, None, tuple[Model, TokenizerWrapper, "VisionProcessor | None"]
|
||||
]:
|
||||
set_wired_limit_for_model(get_weights_size(bound_instance.bound_shard))
|
||||
|
||||
if group is None:
|
||||
logger.info(f"Single device used for {bound_instance.instance}")
|
||||
model_path = build_model_path(bound_instance.bound_shard.model_card.model_id)
|
||||
@@ -199,8 +201,6 @@ def load_mlx_items(
|
||||
f"Time taken to shard and load model: {(end_time - start_time):.2f}s"
|
||||
)
|
||||
|
||||
set_wired_limit_for_model(get_weights_size(bound_instance.bound_shard))
|
||||
|
||||
mx.clear_cache()
|
||||
|
||||
vision_config = bound_instance.bound_shard.model_card.vision
|
||||
@@ -486,6 +486,34 @@ def _needs_dsml_encoding(task_params: TextGenerationTaskParams) -> bool:
|
||||
return "deepseek-v3.2" in task_params.model.lower()
|
||||
|
||||
|
||||
def _needs_v4_encoding(task_params: TextGenerationTaskParams) -> bool:
|
||||
return "deepseek-v4" in task_params.model.lower()
|
||||
|
||||
|
||||
def _v4_reasoning_effort(task_params: TextGenerationTaskParams) -> str | None:
|
||||
effort = task_params.reasoning_effort
|
||||
if effort == "xhigh":
|
||||
return "max"
|
||||
if effort == "high":
|
||||
return "high"
|
||||
return None
|
||||
|
||||
|
||||
_V4_THINK_BLOCK_RE = re.compile(r"<think>.*?</think>", re.DOTALL)
|
||||
|
||||
|
||||
def _strip_v4_thinking_markers(content: str) -> str:
|
||||
"""Remove `<think>…</think>` blocks and any stray `<think>`/`</think>` tags
|
||||
from prior-turn assistant content.
|
||||
|
||||
The V4 encoder drops `reasoning_content` for older turns when
|
||||
`drop_thinking=True`"""
|
||||
if not content:
|
||||
return content
|
||||
cleaned = _V4_THINK_BLOCK_RE.sub("", content)
|
||||
return cleaned.replace("<think>", "").replace("</think>", "")
|
||||
|
||||
|
||||
def consolidate_system_messages(
|
||||
messages: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -549,6 +577,38 @@ def render_chat_template(
|
||||
prompt += partial_assistant_content
|
||||
return prompt
|
||||
|
||||
if _needs_v4_encoding(task_params):
|
||||
from exo.worker.engines.mlx.deepseek_v4_encoding import (
|
||||
encode_messages as encode_messages_v4,
|
||||
)
|
||||
|
||||
v4_messages = [dict(m) for m in formatted_messages]
|
||||
for msg in v4_messages:
|
||||
if msg.get("role") == "assistant":
|
||||
content = msg.get("content")
|
||||
if isinstance(content, str):
|
||||
msg["content"] = _strip_v4_thinking_markers(content)
|
||||
if task_params.tools:
|
||||
for msg in v4_messages:
|
||||
if msg.get("role") in ("system", "developer"):
|
||||
msg["tools"] = task_params.tools
|
||||
break
|
||||
else:
|
||||
v4_messages.insert(
|
||||
0, {"role": "system", "content": "", "tools": task_params.tools}
|
||||
)
|
||||
|
||||
prompt = encode_messages_v4(
|
||||
messages=v4_messages,
|
||||
thinking_mode="chat"
|
||||
if task_params.enable_thinking is False
|
||||
else "thinking",
|
||||
reasoning_effort=_v4_reasoning_effort(task_params),
|
||||
)
|
||||
if partial_assistant_content:
|
||||
prompt += partial_assistant_content
|
||||
return prompt
|
||||
|
||||
for msg in formatted_messages:
|
||||
_normalize_tool_calls(msg)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from collections.abc import Generator, Iterator
|
||||
from collections.abc import Callable, Generator, Iterator
|
||||
from functools import cache
|
||||
from typing import Any
|
||||
|
||||
@@ -23,6 +23,7 @@ from exo.shared.types.chunks import (
|
||||
from exo.shared.types.common import ModelId
|
||||
from exo.shared.types.mlx import Model
|
||||
from exo.shared.types.worker.runner_response import GenerationResponse, ToolCallResponse
|
||||
from exo.worker.engines.mlx.dsml_encoding import parse_dsml_output
|
||||
from exo.worker.engines.mlx.utils_mlx import (
|
||||
detect_thinking_prompt_suffix,
|
||||
)
|
||||
@@ -73,12 +74,10 @@ def apply_all_parsers(
|
||||
) -> Iterator[GenerationChunk | None]:
|
||||
generator = receiver
|
||||
|
||||
normalized_id = model_id.normalize().lower()
|
||||
if issubclass(model_type, GptOssModel):
|
||||
generator = parse_gpt_oss(generator)
|
||||
elif (
|
||||
issubclass(model_type, DeepseekV32Model)
|
||||
and "deepseek" in model_id.normalize().lower()
|
||||
):
|
||||
elif issubclass(model_type, DeepseekV32Model) and "deepseek" in normalized_id:
|
||||
if tokenizer.has_thinking:
|
||||
generator = parse_thinking_models(
|
||||
generator,
|
||||
@@ -87,6 +86,15 @@ def apply_all_parsers(
|
||||
starts_in_thinking=detect_thinking_prompt_suffix(prompt, tokenizer),
|
||||
)
|
||||
generator = parse_deepseek_v32(generator)
|
||||
elif "deepseek-v4" in normalized_id:
|
||||
if tokenizer.has_thinking:
|
||||
generator = parse_thinking_models(
|
||||
generator,
|
||||
tokenizer.think_start,
|
||||
tokenizer.think_end,
|
||||
starts_in_thinking=detect_thinking_prompt_suffix(prompt, tokenizer),
|
||||
)
|
||||
generator = parse_deepseek_v4(generator)
|
||||
else:
|
||||
if tokenizer.has_thinking:
|
||||
generator = parse_thinking_models(
|
||||
@@ -226,6 +234,26 @@ def parse_deepseek_v32(
|
||||
parse_dsml_output,
|
||||
)
|
||||
|
||||
return _parse_dsml_stream(
|
||||
responses, TOOL_CALLS_START, TOOL_CALLS_END, parse_dsml_output
|
||||
)
|
||||
|
||||
|
||||
def parse_deepseek_v4(
|
||||
responses: Generator[GenerationResponse | None],
|
||||
) -> Generator[GenerationResponse | ToolCallResponse | None]:
|
||||
dsml_token = "|DSML|"
|
||||
start = f"<{dsml_token}tool_calls>"
|
||||
end = f"</{dsml_token}tool_calls>"
|
||||
return _parse_dsml_stream(responses, start, end, parse_dsml_output)
|
||||
|
||||
|
||||
def _parse_dsml_stream(
|
||||
responses: Generator[GenerationResponse | None],
|
||||
tool_calls_start: str,
|
||||
tool_calls_end: str,
|
||||
parse_body: Callable[[str], list[ToolCallItem] | None],
|
||||
) -> Generator[GenerationResponse | ToolCallResponse | None]:
|
||||
accumulated = ""
|
||||
in_tool_call = False
|
||||
# Tokens buffered while we detect the start of a DSML block
|
||||
@@ -236,7 +264,7 @@ def parse_deepseek_v32(
|
||||
def _try_parse_tool_call(
|
||||
text: str, response: GenerationResponse
|
||||
) -> ToolCallResponse | GenerationResponse:
|
||||
parsed = parse_dsml_output(text)
|
||||
parsed = parse_body(text)
|
||||
if parsed is not None:
|
||||
return ToolCallResponse(
|
||||
tool_calls=parsed, usage=response.usage, stats=response.stats
|
||||
@@ -256,11 +284,11 @@ def parse_deepseek_v32(
|
||||
tool_call_text += response.text
|
||||
yield (
|
||||
_try_parse_tool_call(tool_call_text, response)
|
||||
if TOOL_CALLS_END in tool_call_text
|
||||
if tool_calls_end in tool_call_text
|
||||
else response.model_copy(update={"text": tool_call_text})
|
||||
)
|
||||
elif TOOL_CALLS_START in response.text and TOOL_CALLS_END in response.text:
|
||||
dsml_start = response.text.index(TOOL_CALLS_START)
|
||||
elif tool_calls_start in response.text and tool_calls_end in response.text:
|
||||
dsml_start = response.text.index(tool_calls_start)
|
||||
before = response.text[:dsml_start]
|
||||
if before:
|
||||
yield response.model_copy(update={"text": before})
|
||||
@@ -269,25 +297,21 @@ def parse_deepseek_v32(
|
||||
yield response
|
||||
break
|
||||
|
||||
# ── Handle tool call accumulation ──
|
||||
if in_tool_call:
|
||||
tool_call_text += response.text
|
||||
if TOOL_CALLS_END in tool_call_text:
|
||||
if tool_calls_end in tool_call_text:
|
||||
yield _try_parse_tool_call(tool_call_text, response)
|
||||
in_tool_call = False
|
||||
tool_call_text = ""
|
||||
continue
|
||||
|
||||
# ── Detect start of tool call block ──
|
||||
accumulated += response.text
|
||||
|
||||
if TOOL_CALLS_START in accumulated:
|
||||
# The start marker might be split across pending_buffer + current token
|
||||
start_idx = accumulated.index(TOOL_CALLS_START)
|
||||
# Yield any pending tokens that are purely before the marker
|
||||
if tool_calls_start in accumulated:
|
||||
start_idx = accumulated.index(tool_calls_start)
|
||||
pre_text = accumulated[:start_idx]
|
||||
# Flush pending buffer tokens that contributed text before the marker
|
||||
if pre_text:
|
||||
# Flush pending buffer tokens that contributed text before the marker
|
||||
for buf_resp in pending_buffer:
|
||||
if not pre_text:
|
||||
break
|
||||
@@ -302,17 +326,14 @@ def parse_deepseek_v32(
|
||||
tool_call_text = accumulated[start_idx:]
|
||||
accumulated = ""
|
||||
|
||||
# Check if the end marker is already present (entire tool call in one token)
|
||||
if TOOL_CALLS_END in tool_call_text:
|
||||
if tool_calls_end in tool_call_text:
|
||||
yield _try_parse_tool_call(tool_call_text, response)
|
||||
tool_call_text = ""
|
||||
else:
|
||||
in_tool_call = True
|
||||
continue
|
||||
|
||||
# Check if accumulated text might be the start of a DSML marker
|
||||
# Buffer tokens if we see a partial match at the end
|
||||
if _could_be_dsml_prefix(accumulated):
|
||||
if _could_be_marker_prefix(accumulated, tool_calls_start):
|
||||
pending_buffer.append(response)
|
||||
continue
|
||||
|
||||
@@ -326,22 +347,12 @@ def parse_deepseek_v32(
|
||||
yield from pending_buffer
|
||||
|
||||
|
||||
def _could_be_dsml_prefix(text: str) -> bool:
|
||||
"""Check if the end of text could be the start of a DSML function_calls marker.
|
||||
|
||||
We look for suffixes of text that are prefixes of the TOOL_CALLS_START pattern.
|
||||
This allows us to buffer tokens until we can determine if a tool call is starting.
|
||||
"""
|
||||
from exo.worker.engines.mlx.dsml_encoding import TOOL_CALLS_START
|
||||
|
||||
# Only check the last portion of text that could overlap with the marker
|
||||
max_check = len(TOOL_CALLS_START)
|
||||
def _could_be_marker_prefix(text: str, marker: str) -> bool:
|
||||
max_check = len(marker)
|
||||
tail = text[-max_check:] if len(text) > max_check else text
|
||||
|
||||
# Check if any suffix of tail is a prefix of TOOL_CALLS_START
|
||||
for i in range(len(tail)):
|
||||
suffix = tail[i:]
|
||||
if TOOL_CALLS_START.startswith(suffix):
|
||||
if marker.startswith(suffix):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -234,6 +234,42 @@ MODEL_CONFIGS = {
|
||||
rope_theta=10000.0,
|
||||
),
|
||||
),
|
||||
"deepseek_v4": dict(
|
||||
module="mlx_lm.models.deepseek_v4",
|
||||
args=dict(
|
||||
model_type="deepseek_v4",
|
||||
vocab_size=256,
|
||||
hidden_size=64,
|
||||
num_hidden_layers=4,
|
||||
num_attention_heads=4,
|
||||
num_key_value_heads=1,
|
||||
q_lora_rank=32,
|
||||
o_lora_rank=32,
|
||||
o_groups=1,
|
||||
head_dim=16,
|
||||
qk_rope_head_dim=8,
|
||||
sliding_window=32,
|
||||
compress_ratios=[0, 0, 4, 0, 0],
|
||||
index_n_heads=4,
|
||||
index_head_dim=16,
|
||||
index_topk=16,
|
||||
moe_intermediate_size=32,
|
||||
n_routed_experts=4,
|
||||
n_shared_experts=1,
|
||||
num_experts_per_tok=2,
|
||||
num_hash_layers=1,
|
||||
hc_mult=1,
|
||||
num_nextn_predict_layers=0,
|
||||
max_position_embeddings=2048,
|
||||
rope_scaling={
|
||||
"beta_fast": 32,
|
||||
"beta_slow": 1,
|
||||
"factor": 2,
|
||||
"original_max_position_embeddings": 1024,
|
||||
"type": "yarn",
|
||||
},
|
||||
),
|
||||
),
|
||||
"gemma4": dict(
|
||||
module="mlx_lm.models.gemma4",
|
||||
args=dict(
|
||||
|
||||
@@ -24,7 +24,7 @@ members = [
|
||||
"exo-bench",
|
||||
"exo-pyo3-bindings",
|
||||
]
|
||||
constraints = [{ name = "transformers", specifier = ">=5.0.0,<5.4.0" }]
|
||||
constraints = [{ name = "transformers", specifier = ">=5.6.2" }]
|
||||
overrides = [
|
||||
{ name = "mlx", marker = "sys_platform == 'darwin'", git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks" },
|
||||
{ name = "mlx", marker = "sys_platform == 'linux'", specifier = "==0.31.1" },
|
||||
@@ -394,8 +394,8 @@ dependencies = [
|
||||
{ name = "loguru", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mflux", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.31.2.dev20260422+ec49d18e", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak#c7010341e1f41ac15815feb5dc55134f44e3b044" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.32.0.dev20260425+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4#0f230c9d108592cea4639a4cac528ce9e1aa0017" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx-vlm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "msgspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "openai-harmony", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
@@ -416,21 +416,21 @@ build = [
|
||||
]
|
||||
cpu = [
|
||||
{ name = "mlx", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.31.2.dev20260422+ec49d18e", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-cpu') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.32.0.dev20260425+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-cpu') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx-cpu", marker = "sys_platform == 'linux'" },
|
||||
{ name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak#c7010341e1f41ac15815feb5dc55134f44e3b044" }, marker = "sys_platform == 'linux'" },
|
||||
{ name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4#0f230c9d108592cea4639a4cac528ce9e1aa0017" }, marker = "sys_platform == 'linux'" },
|
||||
]
|
||||
cuda12 = [
|
||||
{ name = "mlx", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.31.2.dev20260422+ec49d18e", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.32.0.dev20260425+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx-cuda-12", marker = "sys_platform == 'linux'" },
|
||||
{ name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak#c7010341e1f41ac15815feb5dc55134f44e3b044" }, marker = "sys_platform == 'linux'" },
|
||||
{ name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4#0f230c9d108592cea4639a4cac528ce9e1aa0017" }, marker = "sys_platform == 'linux'" },
|
||||
]
|
||||
cuda13 = [
|
||||
{ name = "mlx", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.31.2.dev20260422+ec49d18e", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.32.0.dev20260425+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx-cuda-13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak#c7010341e1f41ac15815feb5dc55134f44e3b044" }, marker = "sys_platform == 'linux'" },
|
||||
{ name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4#0f230c9d108592cea4639a4cac528ce9e1aa0017" }, marker = "sys_platform == 'linux'" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@@ -463,10 +463,10 @@ requires-dist = [
|
||||
{ name = "mlx-cpu", marker = "sys_platform == 'linux' and extra == 'cpu'", specifier = "==0.31.1" },
|
||||
{ name = "mlx-cuda-12", marker = "sys_platform == 'linux' and extra == 'cuda12'", specifier = "==0.31.1" },
|
||||
{ name = "mlx-cuda-13", marker = "sys_platform == 'linux' and extra == 'cuda13'", specifier = "==0.31.1" },
|
||||
{ name = "mlx-lm", marker = "sys_platform == 'darwin'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak" },
|
||||
{ name = "mlx-lm", marker = "sys_platform == 'linux' and extra == 'cpu'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak" },
|
||||
{ name = "mlx-lm", marker = "sys_platform == 'linux' and extra == 'cuda12'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak" },
|
||||
{ name = "mlx-lm", marker = "sys_platform == 'linux' and extra == 'cuda13'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak" },
|
||||
{ name = "mlx-lm", marker = "sys_platform == 'darwin'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4" },
|
||||
{ name = "mlx-lm", marker = "sys_platform == 'linux' and extra == 'cpu'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4" },
|
||||
{ name = "mlx-lm", marker = "sys_platform == 'linux' and extra == 'cuda12'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4" },
|
||||
{ name = "mlx-lm", marker = "sys_platform == 'linux' and extra == 'cuda13'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4" },
|
||||
{ name = "mlx-vlm", specifier = ">=0.3.11" },
|
||||
{ name = "msgspec", specifier = ">=0.19.0" },
|
||||
{ name = "nanobind", marker = "extra == 'build'" },
|
||||
@@ -483,7 +483,7 @@ requires-dist = [
|
||||
{ name = "torch", marker = "(sys_platform == 'linux' and extra == 'cpu' and extra == 'cuda12') or (sys_platform == 'linux' and extra == 'cpu' and extra == 'cuda13')", specifier = ">=2.10.0" },
|
||||
{ name = "torch", marker = "sys_platform == 'linux' and extra == 'cpu' and extra != 'cuda12' and extra != 'cuda13'", specifier = ">=2.10.0", index = "https://download.pytorch.org/whl/cpu" },
|
||||
{ name = "torch", marker = "(sys_platform == 'linux' and extra == 'cpu' and extra == 'cuda13') or (sys_platform == 'linux' and extra == 'cuda12' and extra == 'cuda13')", specifier = ">=2.10.0" },
|
||||
{ name = "transformers", specifier = ">=5.0.0,<5.4.0" },
|
||||
{ name = "transformers", specifier = ">=5.6.2" },
|
||||
{ name = "types-aiofiles", specifier = ">=24.1.0.20250708" },
|
||||
{ name = "zstandard", specifier = ">=0.23.0" },
|
||||
]
|
||||
@@ -1207,7 +1207,7 @@ dependencies = [
|
||||
{ name = "hf-transfer", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "huggingface-hub", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "matplotlib", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.31.2.dev20260422+ec49d18e", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.32.0.dev20260425+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "numpy", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "opencv-python", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "piexif", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
@@ -1257,8 +1257,8 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mlx"
|
||||
version = "0.31.2.dev20260422+ec49d18e"
|
||||
source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }
|
||||
version = "0.32.0.dev20260425+cc3f3e60"
|
||||
source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }
|
||||
resolution-markers = [
|
||||
"sys_platform == 'darwin'",
|
||||
]
|
||||
@@ -1326,7 +1326,7 @@ wheels = [
|
||||
[[package]]
|
||||
name = "mlx-lm"
|
||||
version = "0.31.3"
|
||||
source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak#c7010341e1f41ac15815feb5dc55134f44e3b044" }
|
||||
source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4#0f230c9d108592cea4639a4cac528ce9e1aa0017" }
|
||||
resolution-markers = [
|
||||
"sys_platform == 'darwin'",
|
||||
"sys_platform == 'linux'",
|
||||
@@ -1334,7 +1334,7 @@ resolution-markers = [
|
||||
dependencies = [
|
||||
{ name = "jinja2", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.31.2.dev20260422+ec49d18e", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.32.0.dev20260425+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "numpy", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "protobuf", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "pyyaml", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
@@ -1351,9 +1351,9 @@ dependencies = [
|
||||
{ name = "fastapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "miniaudio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.31.2.dev20260422+ec49d18e", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx", version = "0.32.0.dev20260425+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx-lm", version = "0.31.3", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra != 'extra-3-exo-cpu' and extra != 'extra-3-exo-cuda12' and extra != 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak#c7010341e1f41ac15815feb5dc55134f44e3b044" }, marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4#0f230c9d108592cea4639a4cac528ce9e1aa0017" }, marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "opencv-python", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
@@ -2634,7 +2634,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "transformers"
|
||||
version = "5.2.0"
|
||||
version = "5.6.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
@@ -2645,11 +2645,11 @@ dependencies = [
|
||||
{ name = "safetensors", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "tokenizers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "typer-slim", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
{ name = "typer", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bd/7e/8a0c57d562015e5b16c97c1f0b8e0e92ead2c7c20513225dc12c2043ba9f/transformers-5.2.0.tar.gz", hash = "sha256:0088b8b46ccc9eff1a1dca72b5d618a5ee3b1befc3e418c9512b35dea9f9a650", size = 8618176, upload-time = "2026-02-16T18:54:02.867Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/e9/c6c80a07690142a7d05444271f47b9f3c8aac7dea01d52e1137ee480ad78/transformers-5.6.2.tar.gz", hash = "sha256:e657134c3e5a6bc00a3c35f4e2674bb51adfcd89898495b788a18552bac2b91a", size = 8311867, upload-time = "2026-04-23T18:33:29.332Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/93/79754b0ca486e556c2b95d4f5afc66aaf4b260694f3d6e1b51da2d036691/transformers-5.2.0-py3-none-any.whl", hash = "sha256:9ecaf243dc45bee11a7d93f8caf03746accc0cb069181bbf4ad8566c53e854b4", size = 10403304, upload-time = "2026-02-16T18:53:59.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/95/0b0218149b0d6f14df35f5b8f676fa83df4f19ed253c3cc447107ef86eca/transformers-5.6.2-py3-none-any.whl", hash = "sha256:f8d3a1bb96778fed9b8aabfd0dd6e19843e4b0f2bb6b59f32b8a92051b0f348f", size = 10364898, upload-time = "2026-04-23T18:33:26.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2706,18 +2706,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer-slim"
|
||||
version = "0.24.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typer", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/a7/e6aecc4b4eb59598829a3b5076a93aff291b4fdaa2ded25efc4e1f4d219c/typer_slim-0.24.0.tar.gz", hash = "sha256:f0ed36127183f52ae6ced2ecb2521789995992c521a46083bfcdbb652d22ad34", size = 4776, upload-time = "2026-02-16T22:08:51.2Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/24/5480c20380dfd18cf33d14784096dca45a24eae6102e91d49a718d3b6855/typer_slim-0.24.0-py3-none-any.whl", hash = "sha256:d5d7ee1ee2834d5020c7c616ed5e0d0f29b9a4b1dd283bdebae198ec09778d0e", size = 3394, upload-time = "2026-02-16T22:08:49.92Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-aiofiles"
|
||||
version = "25.1.0.20251011"
|
||||
|
||||
Reference in new issue
Block a user