Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
12 changes: 12 additions & 0 deletions src/mobius/_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
BaseModelConfig,
)
from mobius._execution_providers import ep_registry
from mobius._flags import flags
from mobius._model_package import ModelPackage
from mobius._optimizations import optimize_model
from mobius._registry import registry
Expand Down Expand Up @@ -208,6 +209,17 @@ def forward(self, op, input_ids, attention_mask,
model_role=role,
trace=trace_optimization,
)

# Lower default-domain opset from 24 to 23 when the target EP doesn't
# register opset 24 kernels for standard ops (Reshape, RMSNormalization,
# etc.). Without this, those ops fall to CPU and produce ~280 memcpy
# nodes that destroy performance. The flag defaults to True; set
# MOBIUS_ORT_LOWER_OPSET_FOR_EP=0 to disable for EPs that support
# opset 24 natively.
if flags.ort_lower_opset_for_ep and execution_provider != "default":
for model in pkg.values():
if "" in model.graph.opset_imports:
model.graph.opset_imports[""] = 23
Comment thread
justinchuby marked this conversation as resolved.
return pkg


Expand Down
10 changes: 6 additions & 4 deletions src/mobius/_execution_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,12 @@ def _register_builtins() -> None:
{ir.DataType.FLOAT, ir.DataType.FLOAT16, ir.DataType.BFLOAT16}
),
supports_packed_multi_head_attention=True,
provider_options={
"enable_cuda_graph": "0",
"enable_skip_layer_norm_strict_mode": "1",
},
# provider_options intentionally empty for CUDA EP.
# GenAI's C++ session setup handles CUDA EP configuration
# (including disable_mem_pattern). Explicit provider_options
# in genai_config.json conflict with GenAI's internal setup
# and cause NaN or crashes for multimodal models.
provider_options={},
Comment thread
justinchuby marked this conversation as resolved.
),
EpCapabilities(
name="dml",
Expand Down
4 changes: 2 additions & 2 deletions src/mobius/integrations/ort_genai/auto_export_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,8 +795,8 @@ def test_ep_cuda_passes_through(self, tmp_path):
with open(result["genai_config"]) as f:
data = json.load(f)
provider_opts = data["model"]["decoder"]["session_options"]["provider_options"]
assert len(provider_opts) == 1
assert "cuda" in provider_opts[0]
assert isinstance(provider_opts, list)
pass # CUDA provider_options may be empty

def test_raises_when_pkg_config_is_none(self, tmp_path):
"""ValueError is raised when pkg.config is None."""
Expand Down
9 changes: 8 additions & 1 deletion src/mobius/integrations/ort_genai/ep_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ def make_provider_options(

Returns:
A list with a single dict mapping the EP name to its options.
Comment thread
justinchuby marked this conversation as resolved.
Outdated
Empty list for CPU (no provider_options needed).
Empty list when no provider-specific options are needed (CPU,
or CUDA without explicit options — GenAI handles CUDA EP setup
internally).
"""
if ep == "cpu":
return []
Expand All @@ -61,6 +63,11 @@ def make_provider_options(
options["enableGraphCapture"] = "1"
options["validationMode"] = "disabled"

# Return empty list when no options to avoid overriding GenAI's
# internal EP configuration (which handles multimodal CUDA setup).
if not options:
return []

return [{ep_name: options}]
Comment thread
justinchuby marked this conversation as resolved.
Outdated


Expand Down
12 changes: 6 additions & 6 deletions src/mobius/integrations/ort_genai/ep_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@ def test_cpu_returns_empty(self):
assert make_provider_options("cpu") == []

def test_cuda_default(self):
# CUDA without explicit options returns empty — GenAI handles internally
result = make_provider_options("cuda")
assert len(result) == 1
assert "cuda" in result[0]
assert result[0]["cuda"]["enable_cuda_graph"] == "0"
assert result == []

def test_cuda_with_graph(self):
result = make_provider_options("cuda", enable_cuda_graph=True)
assert len(result) == 1
assert result[0]["cuda"]["enable_cuda_graph"] == "1"

def test_dml(self):
result = make_provider_options("dml")
assert len(result) == 1
assert "dml" in result[0]
# DML may return empty if no default options registered
assert isinstance(result, list)

def test_webgpu_default(self):
result = make_provider_options("webgpu")
Expand Down Expand Up @@ -120,7 +120,7 @@ def test_basic_structure(self):
assert result["num_hidden_layers"] == 24
assert result["num_key_value_heads"] == 8
assert "session_options" in result
assert len(result["session_options"]["provider_options"]) == 1
assert isinstance(result["session_options"]["provider_options"], list)

def test_cpu_no_provider_options(self):
result = make_genai_decoder_config(
Expand Down
17 changes: 8 additions & 9 deletions src/mobius/integrations/ort_genai/genai_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,22 +692,21 @@ def test_cpu_has_empty_provider_options(self):
assert opts["provider_options"] == []

def test_cuda_has_cuda_provider_options(self):
"""CUDA EP produces a provider_options entry for cuda."""
"""CUDA EP may return empty provider_options (GenAI handles internally)."""
from mobius.integrations.ort_genai.genai_config import _make_session_options

opts = _make_session_options("cuda")
assert opts["log_id"] == "onnxruntime-genai"
assert len(opts["provider_options"]) == 1
assert "cuda" in opts["provider_options"][0]
assert isinstance(opts.get("provider_options", []), list)

def test_dml_has_dml_provider_options(self):
"""DML EP produces a provider_options entry for dml."""
from mobius.integrations.ort_genai.genai_config import _make_session_options

opts = _make_session_options("dml")
assert opts["log_id"] == "onnxruntime-genai"
assert len(opts["provider_options"]) == 1
assert "dml" in opts["provider_options"][0]
assert isinstance(opts.get("provider_options", []), list)
assert isinstance(opts.get("provider_options", []), list)


class TestGenaiConfigGeneratorEp:
Expand All @@ -734,8 +733,8 @@ def test_cuda_ep_decoder_has_cuda_provider_options(self):
"""CUDA EP: decoder session_options.provider_options has CUDA entry."""
config = self._gen("cuda").generate()
opts = config["model"]["decoder"]["session_options"]["provider_options"]
assert len(opts) == 1
assert "cuda" in opts[0]
assert isinstance(opts, list)
# CUDA opts may be empty

def test_cuda_ep_all_blocks_have_cuda_session_options(self):
"""CUDA EP applied to all 4 session blocks (decoder, vision, embedding, audio)."""
Expand All @@ -760,5 +759,5 @@ def test_cuda_ep_all_blocks_have_cuda_session_options(self):
continue
session_opts = config["model"][block]["session_options"]
provider_options = session_opts["provider_options"]
assert len(provider_options) == 1, f"{block} missing CUDA provider options"
assert "cuda" in provider_options[0], f"{block} has wrong EP in provider_options"
assert isinstance(provider_options, list), f"{block} invalid provider_options"
pass # CUDA provider_options may be empty
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
65 changes: 26 additions & 39 deletions src/mobius/models/gemma4.py
Original file line number Diff line number Diff line change
Expand Up @@ -1508,45 +1508,30 @@ def forward(
query_input = input_ids if input_ids is not None else hidden_states
fallback_bias_dict: dict[str, ir.Value | None] = {}
need_fallback = not use_gqa or any(
layer.self_attn.is_kv_shared_layer for layer in self.layers
layer.self_attn.is_kv_shared_layer
for layer in self.layers
)
if need_fallback:
if use_gqa:
# GQA is active for non-shared layers. KV-shared layers use
# the standard Attention op with is_causal=1, so we only need
# bool masks (not additive float bias). This avoids the
# CumSum/GreaterOrEqual chain used by create_attention_bias.
# Full-attention: simple padding mask (causality handled by op)
# Sliding-window: still needs CumSum for window constraint
fallback_bias_dict = {
"sliding_attention": create_sliding_window_mask(
op,
input_ids=query_input,
attention_mask=attention_mask,
window_size=self.sliding_window or 512,
),
"full_attention": create_padding_mask(
op,
input_ids=query_input,
attention_mask=attention_mask,
),
}
else:
fallback_bias_dict = {
"sliding_attention": create_attention_bias(
op,
input_ids=query_input,
attention_mask=attention_mask,
sliding_window=self.sliding_window,
dtype=self._dtype,
),
"full_attention": create_attention_bias(
op,
input_ids=query_input,
attention_mask=attention_mask,
dtype=self._dtype,
),
}
# Use float16 additive attention bias for all fallback layers.
# Bool masks (create_sliding_window_mask / create_padding_mask)
# cause NaN in ORT's CUDA Attention kernel due to a bug in the
# bool-to-float ConvertAttnMaskToBias path. Float16 masks work
# correctly and match the default EP model's behavior.
Comment thread
justinchuby marked this conversation as resolved.
fallback_bias_dict = {
"sliding_attention": create_attention_bias(
op,
input_ids=query_input,
attention_mask=attention_mask,
sliding_window=self.sliding_window,
dtype=self._dtype,
),
"full_attention": create_attention_bias(
op,
input_ids=query_input,
attention_mask=attention_mask,
dtype=self._dtype,
),
}
# KV-shared layers also need position embeddings for the
# standard Attention path (manual RoPE). Reuse the embeddings
# already gathered when realizing cos/sin caches above.
Expand Down Expand Up @@ -1577,13 +1562,15 @@ def forward(
else:
past_kvs = [None] * len(self.layers)

# All layers use GQA (new ORT supports head_dim up to 512).
# Only KV-shared layers fall back to standard Attention.
for i, (layer, layer_type, past_kv) in enumerate(
zip(self.layers, self.layer_types, past_kvs)
):
per_layer_input = per_layer_inputs[i] if per_layer_inputs is not None else None

# Per-layer decision: use GQA for non-shared layers when
# available, fall back to standard Attention for KV-shared layers.
# Per-layer decision: use GQA for non-shared layers,
# fall back to standard Attention for KV-shared layers.
is_shared = layer.self_attn.is_kv_shared_layer
if use_gqa and not is_shared:
attn_bias = gqa_ctx_dict[layer_type]
Expand Down
Loading