Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions mergekit/architecture/json_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,9 @@ def _load_architecture_json(text: str) -> ModelArchitecture:
raise RuntimeError(f"Unexpected architecture kind: {data['kind']}")


def _load_all_architectures() -> (
Tuple[List[ModelArchitecture], Dict[str, List[ModelArchitecture]]]
):
def _load_all_architectures() -> Tuple[
List[ModelArchitecture], Dict[str, List[ModelArchitecture]]
]:
architectures: List[ModelArchitecture] = []
for f in importlib.resources.files(mergekit._data.architectures).iterdir():
if f.is_file() and f.name.lower().endswith(".json"):
Expand Down
8 changes: 4 additions & 4 deletions mergekit/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ def local_path(
self, cache_dir: Optional[str] = None, ignore_lora: bool = False
) -> str:
if not ignore_lora:
assert (
self.lora is None
), "LoRA not merged - use .merged() to get a local path"
assert self.lora is None, (
"LoRA not merged - use .merged() to get a local path"
)

path = self.model.path
if not os.path.exists(path):
Expand Down Expand Up @@ -400,7 +400,7 @@ def get_torch_accelerator_count(accelerator_name: Optional[str] = None):
if accelerator_name is not None:
accelerator = torch.device(accelerator_name)
# if user passes the device index in `accelerator_name`, then 1
if accelerator.index != None:
if accelerator.index is not None:
return 1
torch_accelerator_module = getattr(torch, accelerator.type)
else:
Expand Down
6 changes: 3 additions & 3 deletions mergekit/evo/actors.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,9 @@ def evaluate(self, genotype: torch.Tensor) -> dict:

model = self.model.model
if vllm is not None and isinstance(model, vllm.LLM):
assert (
model.llm_engine.parallel_config.world_size == 1
), "Must be single GPU"
assert model.llm_engine.parallel_config.world_size == 1, (
"Must be single GPU"
)
engine = model.llm_engine
if hasattr(engine, "model_executor"):
worker = engine.model_executor.worker
Expand Down
6 changes: 3 additions & 3 deletions mergekit/evo/genome.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ def validate(self):

if self.merge_method == "slerp":
assert not self.smooth, "smooth is not supported for slerp merge method"
assert (
not self.filters
), "tensor name filtering is not supported for slerp merge method"
assert not self.filters, (
"tensor name filtering is not supported for slerp merge method"
)

return self

Expand Down
12 changes: 6 additions & 6 deletions mergekit/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,9 @@ def add_task(self, task: Task, recursive: bool = True) -> "TaskHandle":
_ti_key = (type(task), id(task))
if _ti_key in self._type_id_to_index:
index = self._type_id_to_index[_ti_key]
assert (
self.tasks[index] == task
), "Task modified after being added to universe"
assert self.tasks[index] == task, (
"Task modified after being added to universe"
)
return TaskHandle(self, index)

index = self.task_to_index.setdefault(task, len(self.tasks))
Expand Down Expand Up @@ -322,9 +322,9 @@ def build_schedule(
return ExecutionSchedule(tasks=[], last_use_index={})

universe = targets[0]._universe
assert all(
t._universe is universe for t in targets
), "All tasks must be from the same universe"
assert all(t._universe is universe for t in targets), (
"All tasks must be from the same universe"
)

dummy_handle = TaskHandle(universe, -1)
edge_tups: List[Tuple[TaskHandle, TaskHandle]] = []
Expand Down
4 changes: 2 additions & 2 deletions mergekit/merge_methods/easy_define.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def __merge_method(
(tensor_param.annotation is None)
or (not hasattr(tensor_param.annotation, "__origin__"))
or not (
tensor_param.annotation.__origin__ == list
tensor_param.annotation.__origin__ is list
and tensor_param.annotation.__args__ == (torch.Tensor,)
)
):
Expand Down Expand Up @@ -89,7 +89,7 @@ def __merge_method(
)
elif (
hasattr(arg_info.annotation, "__origin__")
and arg_info.annotation.__origin__ == list
and arg_info.annotation.__origin__ is list
and arg_info.annotation.__args__[0] in (float, int)
):
default_value = arg_info.default
Expand Down
21 changes: 16 additions & 5 deletions mergekit/moe/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-License-Identifier: LGPL-3.0-only

import logging
from typing import List, Optional
from typing import List, Optional, Literal # Ensure Literal is imported

from pydantic import BaseModel

Expand All @@ -29,13 +29,24 @@ class MoEMergeConfig(BaseModel):

base_model: ModelReference
experts: List[Expert]
gate_mode: str = (
"hidden" # possible values: "hidden", "cheap_embed", "random", "uniform_random"
)

# Updated to use Literal for strict validation and added "orthogonal"
gate_mode: Literal[
"hidden",
"cheap_embed",
"random",
"uniform_random",
"orthogonal",
"hidden_avg",
"hidden_last",
] = "hidden"
Comment thread
cursor[bot] marked this conversation as resolved.

# "hidden" uses hidden state vectors for the given prompts for each layer
# "cheap_embed" uses the average of token embeddings for the prompts, same for each layer
# "random" is random
# "random" is standard normal distribution (torch.randn)
# "uniform_random" matches default initialization for torch.nn.Linear
# "orthogonal" ensures gate vectors are orthogonal for better expert specialization

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing validation bypass for orthogonal mode prompts

Medium Severity

The is_bad_config function has an early return for "random" mode to skip prompt validation, but the new "orthogonal" mode (which also doesn't use prompts) wasn't added to this check. Users attempting to use orthogonal initialization without prompts will get the error "Expert X has no positive prompts" even though orthogonal mode generates gate vectors mathematically without using prompts at all.

Fix in Cursor Fix in Web

dtype: Optional[str] = None
experts_per_token: int = 2
shared_experts: Optional[List[Expert]] = None
Expand Down
24 changes: 24 additions & 0 deletions mergekit/moe/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,30 @@ def get_gate_params(
return torch.randn(
(model_cfg.num_hidden_layers, len(experts), model_cfg.hidden_size)
)
# NEW test Orthogonal Initialization
elif mode == "orthogonal":
num_layers = model_cfg.num_hidden_layers
num_experts = len(experts)
hidden_size = model_cfg.hidden_size

# 1. Determine the target dtype
# We check if a specific dtype was requested, otherwise use model default
target_dtype = getattr(model_cfg, "torch_dtype", torch.float16)

# 2. Initialize in float32 for mathematical stability
# We create a list of tensors to match how "hidden" mode returns data
gate_vecs = []
for _ in range(num_layers):
layer_gate = torch.empty((num_experts, hidden_size), dtype=torch.float32)
torch.nn.init.orthogonal_(layer_gate)
# 3. Cast to the target dtype and move to the requested device
gate_vecs.append(
layer_gate.to(
dtype=target_dtype, device=device if device != "auto" else "cpu"
)
)

return gate_vecs

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Orthogonal mode returns list instead of tensor

High Severity

The new orthogonal mode returns gate_vecs as a list of tensors, while all other modes (random, uniform_random, hidden, cheap_embed) return a single tensor with shape (num_layers, num_experts, hidden_size). Callers in moe.py use tensor indexing like gate_vecs[:, :len(...), :] and warn_degenerate_gates expects gate_vecs.shape to exist. This will cause a runtime crash when using orthogonal mode.

Fix in Cursor Fix in Web

elif mode == "uniform_random":
in_features = model_cfg.hidden_size
scale = math.sqrt(1.0 / in_features)
Expand Down
2 changes: 1 addition & 1 deletion mergekit/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def wrapper(*args, **kwargs):
field_type = ShardSizeParamType()

arg_name = field_name.replace("_", "-")
if field_type == bool:
if field_type is bool:
arg_str = f"--{arg_name}/--no-{arg_name}"
else:
arg_str = f"--{arg_name}"
Expand Down
2 changes: 1 addition & 1 deletion mergekit/scripts/tokensurgeon.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ def build_embedding_matrix(
new_tokens.append(token)
stats.to_approximate += 1

donor_tokenizer = transformers.AutoTokenizer.from_pretrained(
transformers.AutoTokenizer.from_pretrained(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Wasteful tokenizer loading with discarded result

Low Severity

The transformers.AutoTokenizer.from_pretrained call loads a donor tokenizer but the result is discarded. This performs network/disk I/O and memory allocation for no purpose. The change removed the donor_tokenizer variable assignment but kept the useless function call - the entire call can be removed since the loaded tokenizer is never used.

Fix in Cursor Fix in Web

options.donor.model.path,
revision=options.donor.model.revision,
trust_remote_code=True,
Expand Down
24 changes: 12 additions & 12 deletions mergekit/tokenizer/embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ def execute(
vocab_size // self.pad_to_multiple_of + 1
) * self.pad_to_multiple_of
embed_size = tensors[models[0]].shape[1]
assert all(
t.shape[1] == embed_size for t in tensors.values()
), "Embedding sizes must match"
assert all(t.shape[1] == embed_size for t in tensors.values()), (
"Embedding sizes must match"
)

dtype = tensors[models[0]].dtype
device = tensors[models[0]].device
Expand Down Expand Up @@ -155,20 +155,20 @@ def compute_default_embedding(
pass
elif isinstance(cfg.source, ModelTokenEmbedding):
model = cfg.source.model
assert (
model in permutations
), f"Model {model} referenced but not part of merge"
assert model in permutations, (
f"Model {model} referenced but not part of merge"
)
p = permutations[model]
src_token_id = cfg.source.token_id
if src_token_id is None:
src_token = cfg.source.token
assert (
src_token in tokenizer_info.original_vocabs[model]
), f"Token {repr(src_token)} not found in model {model}"
assert src_token in tokenizer_info.original_vocabs[model], (
f"Token {repr(src_token)} not found in model {model}"
)
src_token_id = tokenizer_info.original_vocabs[model][src_token]
assert (
src_token_id >= 0 and src_token_id < tensors[model].shape[0]
), f"Token ID {src_token_id} out of range for model {model}"
assert src_token_id >= 0 and src_token_id < tensors[model].shape[0], (
f"Token ID {src_token_id} out of range for model {model}"
)
embed = tensors[model][src_token_id]
elif isinstance(cfg.source, ModelReference):
model = cfg.source
Expand Down
14 changes: 5 additions & 9 deletions mergekit/tokensurgeon/omp.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,10 @@ def batch_omp(
# Project onto existing basis
projections = torch.bmm(
q[:, :, :t].transpose(1, 2), new_atom.unsqueeze(-1)
).squeeze(
-1
) # (B, t)
).squeeze(-1) # (B, t)
residual = new_atom - torch.bmm(
q[:, :, :t], projections.unsqueeze(-1)
).squeeze(
-1
) # (B, D)
).squeeze(-1) # (B, D)
norm = torch.clamp(torch.norm(residual, dim=1), min=eps)
# Update R and Q
r[:, :t, t] = projections
Expand Down Expand Up @@ -218,9 +214,9 @@ def batch_mp_rope(
B, D_a = targets.shape
N, _ = points_a.shape
_, D_b = points_b.shape
assert (
points_a.shape[0] == points_b.shape[0]
), "Number of points in A and B must match"
assert points_a.shape[0] == points_b.shape[0], (
"Number of points in A and B must match"
)
device = targets.device
if k > N:
raise ValueError(f"Cannot select {k} points from {N} candidates")
Expand Down
6 changes: 3 additions & 3 deletions mergekit/tokensurgeon/pca.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ def landmark_pca_approximate(
num_points, d_a = points_a.shape
batch_size, _ = targets.shape
_, d_b = points_b.shape
assert (
points_a.shape[0] == points_b.shape[0]
), "Number of points in A and B must match"
assert points_a.shape[0] == points_b.shape[0], (
"Number of points in A and B must match"
)
assert targets.shape == (batch_size, d_a)

effective_dim = min(d_a, d_b)
Expand Down
2 changes: 1 addition & 1 deletion mergekit/tokensurgeon/rope_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def llama_rope_rotationmat(theta: torch.Tensor) -> torch.Tensor:
theta: Tensor of shape (..., n_heads, head_dim // 2) representing the angles for the rotation.
"""
# theta shape: (..., n_heads, head_dim // 2)
n_heads = theta.shape[-2]
theta.shape[-2]
head_dim = theta.shape[-1] * 2
theta_p = torch.cat([theta, theta], dim=-1)
cos_theta = torch.cos(theta_p)
Expand Down
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,9 @@ testpaths = ["tests"]

[dependency-groups]
test = ["pytest~=8.4.0"]
dev = ["black~=25.1.0", "isort~=6.0.1", "pre-commit~=4.2.0"]
dev = [
"black~=25.1.0",
"isort~=6.0.1",
"pre-commit~=4.2.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Inconsistent indentation in pyproject.toml dev dependencies

Low Severity

Lines 98-99 in the dev dependency list have inconsistent indentation (only 1 space) compared to the surrounding lines which use 4 spaces. This inconsistency may cause TOML parsing issues or at minimum creates confusing formatting.

Fix in Cursor Fix in Web

"ruff>=0.15.0",
]
6 changes: 3 additions & 3 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ def run_and_check_merge(
index_exists = os.path.exists(index_path)
single_shard_exists = os.path.exists(index_path.replace(".index.json", ""))
assert index_exists or single_shard_exists, "No model produced by merge"
assert os.path.exists(
os.path.join(tmpdir, "config.json")
), "No config json produced by merge"
assert os.path.exists(os.path.join(tmpdir, "config.json")), (
"No config json produced by merge"
)

if check_nan:
# check for NaN in output
Expand Down
6 changes: 3 additions & 3 deletions tests/test_chat_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ def check_chat_template(model_path: str, needle: Optional[str] = None):
if needle is None:
assert not tokenizer.chat_template, "Expected no chat template"
return
assert (
tokenizer.chat_template and needle in tokenizer.chat_template
), f"Expected chat template to contain {needle}"
assert tokenizer.chat_template and needle in tokenizer.chat_template, (
f"Expected chat template to contain {needle}"
)


class TestChatTemplate:
Expand Down
Loading
Loading