Skip to content
Closed
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
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
2 changes: 2 additions & 0 deletions src/mobius/integrations/ort_genai/ep_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ def make_provider_options(
options["enableGraphCapture"] = "1"
options["validationMode"] = "disabled"

# Always return the EP entry — GenAI derives its providers list
# from provider_options names. An empty list means CPU-only.
return [{ep_name: options}]


Expand Down
3 changes: 2 additions & 1 deletion src/mobius/integrations/ort_genai/ep_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ def test_cuda_default(self):
result = make_provider_options("cuda")
assert len(result) == 1
assert "cuda" in result[0]
assert result[0]["cuda"]["enable_cuda_graph"] == "0"
assert result[0]["cuda"] == {}

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):
Expand Down
14 changes: 7 additions & 7 deletions src/mobius/integrations/ort_genai/genai_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ 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 includes provider_options entry for GenAI EP registration."""
from mobius.integrations.ort_genai.genai_config import _make_session_options

opts = _make_session_options("cuda")
Expand All @@ -706,8 +706,8 @@ def test_dml_has_dml_provider_options(self):

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 +734,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 +760,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
96 changes: 46 additions & 50 deletions src/mobius/models/gemma4.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,16 @@ def forward(self, op: builder.OpBuilder, hidden_states: ir.Value) -> ir.Value:
# Manual RMSNorm: x / sqrt(mean(x²) + ε), scale = 1.0 (scale-free).
# Using primitive ops avoids ORT's SkipLayerNorm fusion pattern which
# would corrupt the skip shape when an upstream Add uses a 1D bias.
square = op.Mul(hidden_states, hidden_states)
#
# Compute in FP32 to avoid FP16 overflow: values > 256 squared exceed
# the FP16 max (65504), producing inf → mean(inf) → sqrt(inf) → 0.
x_f32 = op.Cast(hidden_states, to=ir.DataType.FLOAT)
square = op.Mul(x_f32, x_f32)
mean_sq = op.ReduceMean(square, op.Constant(value_ints=[-1]), keepdims=1)
eps = op.CastLike(op.Constant(value_float=self.eps), mean_sq)
eps = op.Constant(value_float=self.eps)
rms = op.Sqrt(op.Add(mean_sq, eps))
return op.Div(hidden_states, rms)
result_f32 = op.Div(x_f32, rms)
return op.CastLike(result_f32, hidden_states)


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -770,16 +775,18 @@ def forward(
value_raw = key_raw
else:
value_raw = self.v_proj(op, hidden_states)
# Parameterless per-head V normalisation
# Parameterless per-head V normalisation (FP32 accumulation to
# prevent FP16 overflow when squaring values > 256).
value_states = op.Reshape(
value_raw,
op.Constant(value_ints=[0, 0, self.num_key_value_heads, self.head_dim]),
)
sq = op.Mul(value_states, value_states)
v_f32 = op.Cast(value_states, to=ir.DataType.FLOAT)
sq = op.Mul(v_f32, v_f32)
mean_sq = op.ReduceMean(sq, [-1], keepdims=1)
eps = op.Constant(value_floats=[self._v_norm_eps])
rms = op.Sqrt(op.Add(mean_sq, op.CastLike(eps, mean_sq)))
value_states = op.Div(value_states, rms)
rms = op.Sqrt(op.Add(mean_sq, eps))
value_states = op.CastLike(op.Div(v_f32, rms), value_states)
value_states = op.Reshape(value_states, [0, 0, -1])

# Build GQA attributes
Expand Down Expand Up @@ -843,19 +850,21 @@ def forward(
value_raw = key_raw
else:
value_raw = self.v_proj(op, hidden_states)
# Parameterless per-head V normalisation
# Parameterless per-head V normalisation (FP32 accumulation to
# prevent FP16 overflow when squaring values > 256).
value_states = op.Reshape(
value_raw,
op.Constant(value_ints=[0, 0, self.num_key_value_heads, self.head_dim]),
)
sq = op.Mul(value_states, value_states)
v_f32 = op.Cast(value_states, to=ir.DataType.FLOAT)
sq = op.Mul(v_f32, v_f32)
mean_sq = op.ReduceMean(sq, [-1], keepdims=1)
# Use op.Constant to create a 1D tensor node (not a scalar initializer).
# Scalar Python floats use a type-keyed cache that can fail when upstream
# type information is missing (e.g., after custom ops like com.microsoft.MoE).
eps = op.Constant(value_floats=[self._v_norm_eps])
rms = op.Sqrt(op.Add(mean_sq, op.CastLike(eps, mean_sq)))
value_states = op.Div(value_states, rms)
rms = op.Sqrt(op.Add(mean_sq, eps))
value_states = op.CastLike(op.Div(v_f32, rms), value_states)
value_states = op.Reshape(value_states, [0, 0, -1])

attn_output, present_key, present_value = _apply_attention(
Expand Down Expand Up @@ -1508,45 +1517,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 +1571,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