Skip to content

Commit 7cc9eae

Browse files
gramalingamCopilot
andcommitted
Add native GQA attention support for ORT GenAI compatibility
Add a new GQAAttention component that emits com.microsoft::GroupQueryAttention directly, and a gqa=True mode on CausalLMTask that produces models compatible with the onnxruntime-genai runtime. Key changes: - New GQAAttention component with GQAContext NamedTuple - DecoderLayer accepts pluggable attention_class parameter - CausalLMTask(gqa=True) builds graphs with: - No position_ids input (RoPE fused inside GQA via do_rotary=1) - seqlens_k/total_seq_len computed from attention_mask - cos_cache/sin_cache as graph initializers - com.microsoft opset import - CLI --gqa flag and build() module_kwargs parameter - 10 new GQA-specific tests in build_graph_test.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: G Ramalingam <grama@microsoft.com>
1 parent d4736a0 commit 7cc9eae

8 files changed

Lines changed: 581 additions & 18 deletions

File tree

src/mobius/__main__.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,26 @@ def _cmd_build(args: argparse.Namespace) -> None:
139139
"Remove --task to use --static-cache."
140140
)
141141

142+
# Validate --gqa + --static-cache are mutually exclusive
143+
if args.gqa and args.static_cache:
144+
raise SystemExit("Error: --gqa and --static-cache are mutually exclusive.")
145+
146+
# Validate --gqa + --task compatibility
147+
if args.gqa and args.task is not None:
148+
raise SystemExit(
149+
"Error: --gqa cannot be combined with --task. Remove --task to use --gqa."
150+
)
151+
142152
load_weights = not args.no_weights
143153
task: str | ModelTask | None = args.task
154+
module_kwargs: dict | None = None
144155
if args.static_cache:
145156
task = CausalLMTask(static_cache=True, max_seq_len=args.max_seq_len)
157+
elif args.gqa:
158+
from mobius.components import GQAAttention
159+
160+
task = CausalLMTask(gqa=True)
161+
module_kwargs = {"attention_class": GQAAttention}
146162
trust_remote_code = args.trust_remote_code
147163
output_dir = args.output_dir
148164
os.makedirs(output_dir, exist_ok=True)
@@ -183,7 +199,7 @@ def _cmd_build(args: argparse.Namespace) -> None:
183199
if task is None:
184200
task = _default_task_for_model(model_type)
185201
module_class = registry.get(model_type)
186-
model_module = module_class(config)
202+
model_module = module_class(config, **(module_kwargs or {}))
187203
pkg = build_from_module(model_module, config, task=task)
188204
for name, model in pkg.items():
189205
model.graph.name = f"{config_path}/{name}"
@@ -199,6 +215,7 @@ def _cmd_build(args: argparse.Namespace) -> None:
199215
dtype=dtype_override,
200216
load_weights=load_weights,
201217
trust_remote_code=trust_remote_code,
218+
module_kwargs=module_kwargs,
202219
)
203220

204221
_save_package(pkg, output_dir, args, optimize, component_filter)
@@ -457,6 +474,12 @@ def main(argv: list[str] | None = None) -> None:
457474
help="Maximum sequence length for static cache buffers. "
458475
"Only used with --static-cache. Defaults to max_position_embeddings from config.",
459476
)
477+
build_parser.add_argument(
478+
"--gqa",
479+
action="store_true",
480+
help="Use com.microsoft::GroupQueryAttention with fused RoPE and "
481+
"in-place KV cache support. Compatible with onnxruntime-genai.",
482+
)
460483
build_parser.set_defaults(func=_cmd_build)
461484

462485
# --- build-gguf ---

src/mobius/_builder.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ def build(
246246
dtype: str | ir.DataType | None = None,
247247
load_weights: bool = True,
248248
trust_remote_code: bool = False,
249+
module_kwargs: dict | None = None,
249250
) -> ModelPackage:
250251
"""Build an ONNX :class:`ModelPackage` from a HuggingFace model ID.
251252
@@ -278,6 +279,8 @@ def build(
278279
load_weights: Whether to download and apply weights from HuggingFace.
279280
trust_remote_code: Whether to trust remote code when loading the
280281
HuggingFace config.
282+
module_kwargs: Extra keyword arguments passed to the module class
283+
constructor (e.g. ``{"attention_class": GQAAttention}``).
281284
282285
Returns:
283286
A :class:`ModelPackage` containing the built model(s).
@@ -378,7 +381,7 @@ def build(
378381
if task is None:
379382
task = _default_task_for_model(model_type)
380383

381-
model_module = module_class(config)
384+
model_module = module_class(config, **(module_kwargs or {}))
382385
pkg = build_from_module(model_module, config, task)
383386

384387
# Set graph names

src/mobius/components/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
"EncoderDecoderAttention",
3030
"EncoderLayer",
3131
"FCMLP",
32+
"GQAAttention",
33+
"GQAContext",
3234
"GatedDeltaNet",
3335
"GatedRMSNorm",
3436
"Gemma3MultiModalProjector",
@@ -171,6 +173,7 @@
171173
EncoderDecoderAttention,
172174
)
173175
from mobius.components._gated_deltanet import GatedDeltaNet
176+
from mobius.components._gqa_attention import GQAAttention, GQAContext
174177
from mobius.components._lightning_attention import LightningAttention
175178
from mobius.components._lora import LoRALinear
176179
from mobius.components._mamba_block import Mamba2Block, MambaBlock

src/mobius/components/_decoder.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from mobius._configs import ArchitectureConfig
1313
from mobius.components._attention import Attention, StaticCacheState
14+
from mobius.components._gqa_attention import GQAContext
1415
from mobius.components._mlp import MLP
1516
from mobius.components._rms_norm import RMSNorm
1617

@@ -52,15 +53,18 @@ def __init__(
5253
attention_scale: float | None = None,
5354
post_norm: bool = False,
5455
linear_class: type | None = None,
56+
attention_class: type[nn.Module] | None = None,
5557
):
5658
super().__init__()
5759
if norm_class is None:
5860
norm_class = RMSNorm
61+
if attention_class is None:
62+
attention_class = Attention
5963

6064
self._post_norm = post_norm
6165
self._residual_multiplier = residual_multiplier
6266

63-
self.self_attn = Attention(
67+
self.self_attn = attention_class(
6468
config,
6569
rms_norm_class=norm_class,
6670
scale=attention_scale,
@@ -97,6 +101,17 @@ def forward(
97101
else:
98102
static_cache = None
99103

104+
# GQA mode: when attention_bias is a GQAContext, the attention
105+
# component is GQAAttention and handles masking + RoPE internally.
106+
if isinstance(attention_bias, GQAContext):
107+
gqa_context = attention_bias
108+
return self._forward_gqa(
109+
op,
110+
hidden_states,
111+
gqa_context,
112+
past_key_value,
113+
)
114+
100115
if self._post_norm:
101116
return self._forward_post_norm(
102117
op,
@@ -150,6 +165,42 @@ def _forward_pre_norm(
150165

151166
return hidden_states, present_key_value
152167

168+
def _forward_gqa(
169+
self,
170+
op: builder.OpBuilder,
171+
hidden_states: ir.Value,
172+
gqa_context: GQAContext,
173+
past_key_value: tuple | None,
174+
):
175+
"""Forward pass for GQA mode (pre-norm only).
176+
177+
GQAAttention handles RoPE and causal masking internally, so
178+
position_embeddings and attention_bias are not needed.
179+
"""
180+
residual = hidden_states
181+
hidden_states = self.input_layernorm(op, hidden_states)
182+
183+
attn_output, present_key_value = self.self_attn(
184+
op,
185+
hidden_states=hidden_states,
186+
gqa_context=gqa_context,
187+
past_key_value=past_key_value,
188+
)
189+
190+
if not math.isclose(self._residual_multiplier, 1.0):
191+
attn_output = op.Mul(attn_output, self._residual_multiplier)
192+
hidden_states = op.Add(residual, attn_output)
193+
194+
residual = hidden_states
195+
hidden_states = self.post_attention_layernorm(op, hidden_states)
196+
hidden_states = self.mlp(op, hidden_states)
197+
198+
if not math.isclose(self._residual_multiplier, 1.0):
199+
hidden_states = op.Mul(hidden_states, self._residual_multiplier)
200+
hidden_states = op.Add(residual, hidden_states)
201+
202+
return hidden_states, present_key_value
203+
153204
def _forward_post_norm(
154205
self,
155206
op: builder.OpBuilder,
@@ -185,6 +236,7 @@ def create_decoder_layer(
185236
norm_class: type[nn.Module] | None = None,
186237
post_norm: bool = False,
187238
linear_class: type | None = None,
239+
attention_class: type[nn.Module] | None = None,
188240
) -> DecoderLayer:
189241
"""Config-driven factory for creating decoder layers.
190242
@@ -200,6 +252,8 @@ def create_decoder_layer(
200252
post_norm: If True, use post-norm residual connections (OLMo-2 style).
201253
linear_class: Factory callable for projection layers. Pass a LoRA
202254
factory for LoRA-adapted layers.
255+
attention_class: Attention module class override (default: Attention).
256+
Pass GQAAttention for ORT GenAI-compatible models.
203257
204258
Returns:
205259
A configured DecoderLayer instance.
@@ -214,6 +268,7 @@ def create_decoder_layer(
214268
attention_scale=attention_scale,
215269
post_norm=post_norm,
216270
linear_class=linear_class,
271+
attention_class=attention_class,
217272
)
218273

219274

0 commit comments

Comments
 (0)