QyrouNnet-AI commited on
Commit
9ce532f
·
verified ·
1 Parent(s): 7b0c37b

Release Qyrou-1-EXP-Base: 65M hybrid GQA/conv model (serving-complete)

Browse files
README.md CHANGED
@@ -1,3 +1,4 @@
 
1
  ---
2
  license: apache-2.0
3
  language:
@@ -28,17 +29,42 @@ It is also designed to support intelligent features in applications such as the
28
 
29
  Qyrou-Vega models are the mid-sized variants within the Qyrou model family. They are designed to handle a broad range of tasks typically supported by small language models, including summarization, fill-in-the-middle (FIM), basic code completion, text generation, mathematical reasoning, and more.
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  ## Model Architecture
32
 
33
  | Component | Qyrou specification |
34
  |---|---|
35
  | Model type | Qyrou; decoder-only hybrid causal language model |
36
- | Total parameters | 65,302,016 (~65.3M) |
37
  | Vocabulary | 20,000 tokens |
38
  | Context length | 2,048 tokens |
39
  | Hidden size | 512 |
40
  | Blocks | 21 |
41
  | Normalization | Pre-RMSNorm |
 
42
  | Positional encoding | RoPE (θ = 10,000) |
43
  | Token embeddings / LM head | Tied |
44
  | Attention blocks | 17 |
@@ -57,12 +83,7 @@ Qyrou-Vega models are the mid-sized variants within the Qyrou model family. They
57
  | Biases | None |
58
  | Per-block structure | `x = x + Mixer(RMSNorm(x))` → `x = x + SwiGLU_FFN(RMSNorm(x))` |
59
 
60
- Qyrou-Vega is a 65.3M-parameter language model built around the `qyrou-arch` architecture. It was trained with a 2,048-token context window, although it could be extended beyond that with updated RoPE settings and cache support; there is no confirmed maximum yet (Although training beyond a sequence length of 2k ould degrade a 65M SLM). The model has 21 layers: 17 use grouped-query attention, while 4 use causal convolutions in layers 4, 9, 14, and 19. Each layer uses RMSNorm and SwiGLU, with a hidden size of 512, a 20,000-token vocabulary, eight attention heads, two key/value heads, and tied input and output embeddings.
61
-
62
-
63
-
64
-
65
-
66
 
67
  ## Tokenizer
68
 
@@ -99,22 +120,18 @@ Some control markers are intentionally represented as atomic regular-text tokens
99
 
100
  Users are free to fine-tune Qyrou-1-EXP for their own tasks, including domains that require additional control symbols or structured token sequences. If a required token or marker is not currently available, please open a discussion in this model repository describing the intended use case and the proposed token. Feedback from these experiments will be considered for future tokenizer revisions and subsequent model versions.
101
 
 
102
 
 
103
 
 
104
 
 
105
 
 
106
 
107
-
108
- ## Nvidia GPU Acceleration
109
-
110
- We built this experimental model to run best on NVIDIA hardware, so **use an NVIDIA GPU whenever possible.** For maximum performance, enable BF16, cuDNN or Flash Attention, torch. compile, fused QKV and SwiGLU projections, and the included custom Triton kernels. Install Liger Kernel and Cut Cross-Entropy to enable the additional optimized paths. Together, these features reduce memory usage, kernel-launch overhead, and unnecessary computation while taking advantage of NVIDIA Tensor Cores.
111
-
112
- *llama.cpp support will take some time. I’m focusing on getting the architecture working correctly first, then I’ll roll out performance optimizations gradually as the full model family is released.*
113
 
114
  ## Limitations
115
 
116
  Qyrou-Vega was pretrained on a diverse dataset; however, as a small language model (SLM), it does not match the capabilities or performance of larger models trained on substantially larger and more diverse corpora. This release represents the first-stage pretraining (PT) run and is provided strictly for experimental and research purposes. As the first experimental base model in the Qyrou-1 family, it is intended primarily for experimentation and fine-tuning and is not recommended for production use until an official stable release is released by the Qyrou organization. Furthermore, Qyrou-Vega is a base model and has not been instruction-tuned, meaning it is not optimized to follow user instructions or structured response formats and may produce inconsistent or unstable outputs until properly fine-tuned. By using this model, you acknowledge its experimental nature and assume full responsibility for its use. The Qyrou organization and its contributors are not liable for any damages, losses, or consequences arising from the use or misuse of this model.
117
-
118
-
119
-
120
-
 
1
+
2
  ---
3
  license: apache-2.0
4
  language:
 
29
 
30
  Qyrou-Vega models are the mid-sized variants within the Qyrou model family. They are designed to handle a broad range of tasks typically supported by small language models, including summarization, fill-in-the-middle (FIM), basic code completion, text generation, mathematical reasoning, and more.
31
 
32
+ ## Quickstart
33
+
34
+ This model is compatible with standard Hugging Face Transformers workflows and may be used as a remote-code model. Before proceeding, install the required Python dependencies. An NVIDIA GPU with a recent driver is strongly recommended for optimal performance; additional guidance is provided in the GPU acceleration section below.
35
+
36
+ ```bash
37
+ pip install --upgrade transformers torch tokenizers
38
+ ```
39
+
40
+ ```python
41
+ from transformers import AutoModelForCausalLM, AutoTokenizer
42
+
43
+ model = AutoModelForCausalLM.from_pretrained("Qyrou/Qyrou-1-EXP-Base", trust_remote_code=True)
44
+ tokenizer = AutoTokenizer.from_pretrained("Qyrou/Qyrou-1-EXP-Base")
45
+ model = model.to("cuda").to(torch.bfloat16) if torch.cuda.is_available() else model.eval()
46
+
47
+ inputs = tokenizer("The capital of France is", return_tensors="pt").to(model.device)
48
+ outputs = model.generate(**inputs, max_new_tokens=64, do_sample=False)
49
+ print(tokenizer.decode(outputs[0], skip_special_tokens=True))
50
+ ```
51
+
52
+ Generation is supported through the standard `GenerationMixin` interface, including greedy decoding, sampling, beam search, and batch decoding with padding. The hybrid incremental cache, which combines attention key/value states with short-convolution state, is created and updated automatically; as a result, no explicit cache management is required on the caller side. Generation with `use_cache=True` is functionally equivalent to a full forward pass, including for right-padded and unequal-length batches.
53
+
54
+ The serving contract follows standard Transformers conventions. The model accepts `output_hidden_states`, `output_attentions`, `attention_mask`, `position_ids`, `past_key_values`, `cache_position`, `use_cache`, and the standard framework arguments. Attention weights are returned for every GQA layer when `output_attentions=True`, and hidden states are returned after the embedding layer and after each block. Unsupported arguments are rejected with clear errors rather than being silently ignored, which helps surface integration issues early.
55
+
56
  ## Model Architecture
57
 
58
  | Component | Qyrou specification |
59
  |---|---|
60
  | Model type | Qyrou; decoder-only hybrid causal language model |
61
+ | Total parameters | 65,304,192 (~65.3M) |
62
  | Vocabulary | 20,000 tokens |
63
  | Context length | 2,048 tokens |
64
  | Hidden size | 512 |
65
  | Blocks | 21 |
66
  | Normalization | Pre-RMSNorm |
67
+ | Query/key states | QK-norm (per-head RMSNorm on query and key states) |
68
  | Positional encoding | RoPE (θ = 10,000) |
69
  | Token embeddings / LM head | Tied |
70
  | Attention blocks | 17 |
 
83
  | Biases | None |
84
  | Per-block structure | `x = x + Mixer(RMSNorm(x))` → `x = x + SwiGLU_FFN(RMSNorm(x))` |
85
 
86
+ Qyrou-Vega is a 65.3M-parameter language model built around the `qyrou-arch` architecture. It was trained with a 2,048-token context window, although it could be extended beyond that with updated RoPE settings and cache support; there is no confirmed maximum yet (Although training beyond a sequence length of 2k could degrade a 65M SLM). The model has 21 layers: 17 use grouped-query attention, while 4 use causal convolutions in layers 4, 9, 14, and 19. Each layer uses RMSNorm and SwiGLU, with a hidden size of 512, a 20,000-token vocabulary, eight attention heads, two key/value heads, and tied input and output embeddings. Query and key states are normalized with per-head RMSNorm (QK-norm) before the rotary embedding is applied.
 
 
 
 
 
87
 
88
  ## Tokenizer
89
 
 
120
 
121
  Users are free to fine-tune Qyrou-1-EXP for their own tasks, including domains that require additional control symbols or structured token sequences. If a required token or marker is not currently available, please open a discussion in this model repository describing the intended use case and the proposed token. Feedback from these experiments will be considered for future tokenizer revisions and subsequent model versions.
122
 
123
+ ## Nvidia GPU Acceleration
124
 
125
+ We built this experimental model to run best on NVIDIA hardware, so **use an NVIDIA GPU whenever possible.** For maximum performance, enable BF16, cuDNN or Flash Attention, torch.compile, fused QKV and SwiGLU projections, and the included custom Triton kernels. Install Liger Kernel and Cut Cross-Entropy to enable the additional optimized paths. Together, these features reduce memory usage, kernel-launch overhead, and unnecessary computation while taking advantage of NVIDIA Tensor Cores.
126
 
127
+ *llama.cpp support will take some time. I'm focusing on getting the architecture working correctly first, then I'll roll out performance optimizations gradually as the full model family is released.*
128
 
129
+ ## Development
130
 
131
+ The repository is organized in a relatively straightforward manner. The implementation is contained in the `qyrou_arch/` directory, the test suite is located in the `tests/` directory, and the standalone utilities are stored in the `scripts/` directory. The test suite may be executed with `pytest` from the `source/` directory. It evaluates the core behavior of the model, including numerical parity for each layer against manual references, cached and uncached forward passes (including left-padded, right-padded, and uneven batch sizes), the serving interface, and the CUDA-gated Triton kernel paths. The CPU-based tests are expected to pass directly, while the Triton-specific tests assess FP32 and BF16 behavior, edge cases such as tail widths and non-contiguous inputs, and larger GPU batch sizes.
132
 
133
+ The `scripts/benchmark_micro.py` script functions as a lightweight benchmarking utility for the primary kernels, including packed QKV and SwiGLU GEMMs, RMSNorm, SDPA backends, and the cross-entropy path. It produces a JSON report containing median timings, which is useful for identifying regressions across different hardware configurations. CUDA is required to run this script.
 
 
 
 
 
134
 
135
  ## Limitations
136
 
137
  Qyrou-Vega was pretrained on a diverse dataset; however, as a small language model (SLM), it does not match the capabilities or performance of larger models trained on substantially larger and more diverse corpora. This release represents the first-stage pretraining (PT) run and is provided strictly for experimental and research purposes. As the first experimental base model in the Qyrou-1 family, it is intended primarily for experimentation and fine-tuning and is not recommended for production use until an official stable release is released by the Qyrou organization. Furthermore, Qyrou-Vega is a base model and has not been instruction-tuned, meaning it is not optimized to follow user instructions or structured response formats and may produce inconsistent or unstable outputs until properly fine-tuned. By using this model, you acknowledge its experimental nature and assume full responsibility for its use. The Qyrou organization and its contributors are not liable for any damages, losses, or consequences arising from the use or misuse of this model.
 
 
 
 
modeling_qyrou_arch.py CHANGED
@@ -13,6 +13,19 @@ from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutpu
13
  from .cache import QyrouArchHybridCache
14
  from .configuration_qyrou_arch import QyrouArchConfig
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  try:
17
  from .triton_kernels import PackedSwiGLUFunction
18
  except (ImportError, RuntimeError):
@@ -60,27 +73,6 @@ def _apply_rope(
60
  return torch.stack((even * cos - odd * sin, even * sin + odd * cos), dim=-1).flatten(-2)
61
 
62
 
63
- def _last_valid_state(
64
- x: torch.Tensor,
65
- mask: torch.Tensor | None,
66
- state_length: int,
67
- ) -> torch.Tensor:
68
- if state_length == 0:
69
- return x[:, :0]
70
- if mask is None:
71
- return F.pad(x, (0, 0, max(0, state_length - x.shape[1]), 0))[:, -state_length:]
72
- result = torch.zeros(
73
- (x.shape[0], state_length, x.shape[2]),
74
- dtype=x.dtype,
75
- device=x.device,
76
- )
77
- for batch_index in range(x.shape[0]):
78
- valid = x[batch_index][mask[batch_index].bool()]
79
- valid = valid[-state_length:]
80
- result[batch_index, -valid.shape[0] :] = valid
81
- return result
82
-
83
-
84
  class CausalGQA(nn.Module):
85
  def __init__(self, config: QyrouArchConfig, layer_idx: int) -> None:
86
  super().__init__()
@@ -109,7 +101,8 @@ class CausalGQA(nn.Module):
109
  attention_mask: torch.Tensor | None,
110
  cache: QyrouArchHybridCache | None,
111
  cache_position: torch.Tensor,
112
- ) -> torch.Tensor:
 
113
  batch, query_length, _ = x.shape
114
  if hasattr(self, "qkv_proj"):
115
  q, k, v = self.qkv_proj(x).split((self.q_size, self.kv_size, self.kv_size), dim=-1)
@@ -135,7 +128,22 @@ class CausalGQA(nn.Module):
135
  if attention_mask.shape[-1] < key_length:
136
  raise ValueError("attention_mask is shorter than the cached key sequence")
137
  mask = mask & attention_mask[:, None, None, :key_length].bool()
138
- if q.is_cuda and self.attention_backend in {"cudnn", "flash"}:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  backends = (
140
  [SDPBackend.CUDNN_ATTENTION, SDPBackend.FLASH_ATTENTION, SDPBackend.MATH]
141
  if self.attention_backend == "cudnn"
@@ -163,7 +171,7 @@ class CausalGQA(nn.Module):
163
  enable_gqa=True,
164
  )
165
  output = output.transpose(1, 2).contiguous().view(batch, query_length, -1)
166
- return self.o_proj(output)
167
 
168
 
169
  class CausalConvMixer(nn.Module):
@@ -187,8 +195,9 @@ class CausalConvMixer(nn.Module):
187
  _rope: tuple[torch.Tensor, torch.Tensor],
188
  attention_mask: torch.Tensor | None,
189
  cache: QyrouArchHybridCache | None,
190
- _cache_position: torch.Tensor,
191
- ) -> torch.Tensor:
 
192
  state_length = self.kernel_size - 1
193
  query_mask = attention_mask[:, -x.shape[1] :] if attention_mask is not None else None
194
  if query_mask is not None:
@@ -205,24 +214,9 @@ class CausalConvMixer(nn.Module):
205
  )
206
  combined = torch.cat((previous, x), dim=1)
207
  conv_input = combined.transpose(1, 2)
208
- combined_mask = None
209
- if query_mask is not None:
210
- combined_mask = torch.cat(
211
- (
212
- torch.ones(
213
- (x.shape[0], state_length),
214
- dtype=query_mask.dtype,
215
- device=query_mask.device,
216
- ),
217
- query_mask,
218
- ),
219
- dim=1,
220
- )
221
- cache.update_convolution(
222
- self.layer_idx,
223
- _last_valid_state(combined, combined_mask, state_length),
224
- )
225
- return self.pointwise(self.depthwise(conv_input).transpose(1, 2))
226
 
227
 
228
  class SwiGLU(nn.Module):
@@ -280,9 +274,39 @@ class QyrouArchBlock(nn.Module):
280
  attention_mask: torch.Tensor | None,
281
  cache: QyrouArchHybridCache | None,
282
  cache_position: torch.Tensor,
283
- ) -> torch.Tensor:
284
- x = x + self.mixer(self.mixer_norm(x), rope, attention_mask, cache, cache_position)
285
- return x + self.ffn(self.ffn_norm(x))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
 
288
  class QyrouArchPreTrainedModel(PreTrainedModel):
@@ -342,9 +366,14 @@ class QyrouArchModel(QyrouArchPreTrainedModel):
342
  inputs_embeds: torch.Tensor | None = None,
343
  use_cache: bool | None = None,
344
  cache_position: torch.LongTensor | None = None,
 
 
345
  return_dict: bool | None = None,
346
- **_: Any,
347
  ) -> BaseModelOutputWithPast | tuple[torch.Tensor, QyrouArchHybridCache | None]:
 
 
 
348
  if (input_ids is None) == (inputs_embeds is None):
349
  raise ValueError("Pass exactly one of input_ids or inputs_embeds")
350
  hidden = self.embed_tokens(input_ids) if inputs_embeds is None else inputs_embeds
@@ -353,8 +382,18 @@ class QyrouArchModel(QyrouArchPreTrainedModel):
353
  batch, query_length, _ = hidden.shape
354
  use_cache = self.config.use_cache if use_cache is None else use_cache
355
  return_dict = self.config.return_dict if return_dict is None else return_dict
 
 
 
 
 
 
 
 
356
  if use_cache and past_key_values is None:
357
  past_key_values = QyrouArchHybridCache(self.config.num_hidden_layers)
 
 
358
  cache = past_key_values if use_cache else None
359
  past_length = cache.get_seq_length() if cache is not None else 0
360
  if cache_position is None:
@@ -369,14 +408,32 @@ class QyrouArchModel(QyrouArchPreTrainedModel):
369
  else:
370
  position_ids = cache_position.unsqueeze(0).expand(batch, -1)
371
  rope = self._rope(position_ids, hidden.dtype)
 
 
372
  for layer in self.layers:
373
- hidden = layer(hidden, rope, attention_mask, cache, cache_position)
 
 
 
 
 
 
 
 
 
 
 
374
  hidden = self.norm(hidden)
375
  if cache is not None:
376
  cache.finish_step(cache_position)
377
  if not return_dict:
378
  return hidden, cache
379
- return BaseModelOutputWithPast(last_hidden_state=hidden, past_key_values=cache)
 
 
 
 
 
380
 
381
 
382
  class QyrouArchForCausalLM(QyrouArchPreTrainedModel, GenerationMixin):
@@ -411,10 +468,15 @@ class QyrouArchForCausalLM(QyrouArchPreTrainedModel, GenerationMixin):
411
  labels: torch.LongTensor | None = None,
412
  use_cache: bool | None = None,
413
  cache_position: torch.LongTensor | None = None,
 
 
414
  return_logits: bool = True,
415
  return_dict: bool | None = None,
416
  **kwargs: Any,
417
  ) -> CausalLMOutputWithPast | tuple[Any, ...]:
 
 
 
418
  return_dict = self.config.return_dict if return_dict is None else return_dict
419
  outputs = self.model(
420
  input_ids=input_ids,
@@ -424,6 +486,8 @@ class QyrouArchForCausalLM(QyrouArchPreTrainedModel, GenerationMixin):
424
  inputs_embeds=inputs_embeds,
425
  use_cache=use_cache,
426
  cache_position=cache_position,
 
 
427
  return_dict=True,
428
  **kwargs,
429
  )
@@ -458,6 +522,8 @@ class QyrouArchForCausalLM(QyrouArchPreTrainedModel, GenerationMixin):
458
  loss=loss,
459
  logits=logits,
460
  past_key_values=outputs.past_key_values,
 
 
461
  )
462
 
463
  def prepare_inputs_for_generation(
 
13
  from .cache import QyrouArchHybridCache
14
  from .configuration_qyrou_arch import QyrouArchConfig
15
 
16
+ _FRAMEWORK_KWARGS = frozenset(
17
+ {
18
+ "num_items_in_batch",
19
+ "output_router_logits",
20
+ "cu_seq_lens_q",
21
+ "cu_seq_lens_k",
22
+ "max_length_q",
23
+ "max_length_k",
24
+ "is_causal",
25
+ "seq_idx",
26
+ }
27
+ )
28
+
29
  try:
30
  from .triton_kernels import PackedSwiGLUFunction
31
  except (ImportError, RuntimeError):
 
73
  return torch.stack((even * cos - odd * sin, even * sin + odd * cos), dim=-1).flatten(-2)
74
 
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  class CausalGQA(nn.Module):
77
  def __init__(self, config: QyrouArchConfig, layer_idx: int) -> None:
78
  super().__init__()
 
101
  attention_mask: torch.Tensor | None,
102
  cache: QyrouArchHybridCache | None,
103
  cache_position: torch.Tensor,
104
+ output_attentions: bool = False,
105
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
106
  batch, query_length, _ = x.shape
107
  if hasattr(self, "qkv_proj"):
108
  q, k, v = self.qkv_proj(x).split((self.q_size, self.kv_size, self.kv_size), dim=-1)
 
128
  if attention_mask.shape[-1] < key_length:
129
  raise ValueError("attention_mask is shorter than the cached key sequence")
130
  mask = mask & attention_mask[:, None, None, :key_length].bool()
131
+ if output_attentions:
132
+ if mask is None:
133
+ mask = torch.tril(
134
+ torch.ones((1, 1, query_length, key_length), dtype=torch.bool, device=x.device)
135
+ ).expand(batch, 1, query_length, key_length)
136
+ query_heads = q.shape[1]
137
+ kv_heads = k.shape[1]
138
+ if kv_heads != query_heads:
139
+ repeat = query_heads // kv_heads
140
+ k = k.repeat_interleave(repeat, dim=1)
141
+ v = v.repeat_interleave(repeat, dim=1)
142
+ scores = torch.matmul(q, k.transpose(-2, -1)) * (self.head_dim**-0.5)
143
+ scores = scores.masked_fill(~mask, torch.finfo(scores.dtype).min)
144
+ weights = torch.softmax(scores, dim=-1)
145
+ output = torch.matmul(weights, v)
146
+ elif q.is_cuda and self.attention_backend in {"cudnn", "flash"}:
147
  backends = (
148
  [SDPBackend.CUDNN_ATTENTION, SDPBackend.FLASH_ATTENTION, SDPBackend.MATH]
149
  if self.attention_backend == "cudnn"
 
171
  enable_gqa=True,
172
  )
173
  output = output.transpose(1, 2).contiguous().view(batch, query_length, -1)
174
+ return self.o_proj(output), weights if output_attentions else None
175
 
176
 
177
  class CausalConvMixer(nn.Module):
 
195
  _rope: tuple[torch.Tensor, torch.Tensor],
196
  attention_mask: torch.Tensor | None,
197
  cache: QyrouArchHybridCache | None,
198
+ cache_position: torch.Tensor,
199
+ _output_attentions: bool = False,
200
+ ) -> tuple[torch.Tensor, None]:
201
  state_length = self.kernel_size - 1
202
  query_mask = attention_mask[:, -x.shape[1] :] if attention_mask is not None else None
203
  if query_mask is not None:
 
214
  )
215
  combined = torch.cat((previous, x), dim=1)
216
  conv_input = combined.transpose(1, 2)
217
+ cache.update_convolution(self.layer_idx, combined[:, -state_length:])
218
+ output = self.pointwise(self.depthwise(conv_input).transpose(1, 2))
219
+ return output, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
 
222
  class SwiGLU(nn.Module):
 
274
  attention_mask: torch.Tensor | None,
275
  cache: QyrouArchHybridCache | None,
276
  cache_position: torch.Tensor,
277
+ output_attentions: bool = False,
278
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
279
+ mixer_output, weights = self.mixer(
280
+ self.mixer_norm(x),
281
+ rope,
282
+ attention_mask,
283
+ cache,
284
+ cache_position,
285
+ output_attentions,
286
+ )
287
+ x = x + mixer_output
288
+ return x + self.ffn(self.ffn_norm(x)), weights
289
+
290
+
291
+ def _as_hybrid_cache(past_key_values: Any, num_layers: int) -> QyrouArchHybridCache:
292
+ cache = QyrouArchHybridCache(num_layers)
293
+ layers = getattr(past_key_values, "layers", None)
294
+ if layers is not None:
295
+ for index, layer in enumerate(layers[:num_layers]):
296
+ if getattr(layer, "is_initialized", False):
297
+ cache.attention[index] = (layer.keys, layer.values)
298
+ else:
299
+ key_cache = getattr(past_key_values, "key_cache", None)
300
+ if key_cache is None:
301
+ raise TypeError(
302
+ "past_key_values must be a QyrouArchHybridCache or a framework Cache instance"
303
+ )
304
+ value_cache = past_key_values.value_cache
305
+ for index in range(min(len(key_cache), num_layers)):
306
+ if key_cache[index] is not None:
307
+ cache.attention[index] = (key_cache[index], value_cache[index])
308
+ cache.seen_tokens = past_key_values.get_seq_length()
309
+ return cache
310
 
311
 
312
  class QyrouArchPreTrainedModel(PreTrainedModel):
 
366
  inputs_embeds: torch.Tensor | None = None,
367
  use_cache: bool | None = None,
368
  cache_position: torch.LongTensor | None = None,
369
+ output_attentions: bool | None = None,
370
+ output_hidden_states: bool | None = None,
371
  return_dict: bool | None = None,
372
+ **kwargs: Any,
373
  ) -> BaseModelOutputWithPast | tuple[torch.Tensor, QyrouArchHybridCache | None]:
374
+ unsupported = set(kwargs) - _FRAMEWORK_KWARGS
375
+ if unsupported:
376
+ raise ValueError(f"Unsupported model arguments: {sorted(unsupported)}")
377
  if (input_ids is None) == (inputs_embeds is None):
378
  raise ValueError("Pass exactly one of input_ids or inputs_embeds")
379
  hidden = self.embed_tokens(input_ids) if inputs_embeds is None else inputs_embeds
 
382
  batch, query_length, _ = hidden.shape
383
  use_cache = self.config.use_cache if use_cache is None else use_cache
384
  return_dict = self.config.return_dict if return_dict is None else return_dict
385
+ output_attentions = bool(
386
+ getattr(self.config, "output_attentions", False) if output_attentions is None else output_attentions
387
+ )
388
+ output_hidden_states = bool(
389
+ getattr(self.config, "output_hidden_states", False)
390
+ if output_hidden_states is None
391
+ else output_hidden_states
392
+ )
393
  if use_cache and past_key_values is None:
394
  past_key_values = QyrouArchHybridCache(self.config.num_hidden_layers)
395
+ if past_key_values is not None and not isinstance(past_key_values, QyrouArchHybridCache):
396
+ past_key_values = _as_hybrid_cache(past_key_values, self.config.num_hidden_layers)
397
  cache = past_key_values if use_cache else None
398
  past_length = cache.get_seq_length() if cache is not None else 0
399
  if cache_position is None:
 
408
  else:
409
  position_ids = cache_position.unsqueeze(0).expand(batch, -1)
410
  rope = self._rope(position_ids, hidden.dtype)
411
+ hidden_states = (hidden,) if output_hidden_states else None
412
+ attentions = () if output_attentions else None
413
  for layer in self.layers:
414
+ hidden, weights = layer(
415
+ hidden,
416
+ rope,
417
+ attention_mask,
418
+ cache,
419
+ cache_position,
420
+ output_attentions,
421
+ )
422
+ if hidden_states is not None:
423
+ hidden_states += (hidden,)
424
+ if attentions is not None:
425
+ attentions += (weights,)
426
  hidden = self.norm(hidden)
427
  if cache is not None:
428
  cache.finish_step(cache_position)
429
  if not return_dict:
430
  return hidden, cache
431
+ return BaseModelOutputWithPast(
432
+ last_hidden_state=hidden,
433
+ past_key_values=cache,
434
+ hidden_states=hidden_states,
435
+ attentions=attentions,
436
+ )
437
 
438
 
439
  class QyrouArchForCausalLM(QyrouArchPreTrainedModel, GenerationMixin):
 
468
  labels: torch.LongTensor | None = None,
469
  use_cache: bool | None = None,
470
  cache_position: torch.LongTensor | None = None,
471
+ output_attentions: bool | None = None,
472
+ output_hidden_states: bool | None = None,
473
  return_logits: bool = True,
474
  return_dict: bool | None = None,
475
  **kwargs: Any,
476
  ) -> CausalLMOutputWithPast | tuple[Any, ...]:
477
+ unsupported = set(kwargs) - _FRAMEWORK_KWARGS
478
+ if unsupported:
479
+ raise ValueError(f"Unsupported model arguments: {sorted(unsupported)}")
480
  return_dict = self.config.return_dict if return_dict is None else return_dict
481
  outputs = self.model(
482
  input_ids=input_ids,
 
486
  inputs_embeds=inputs_embeds,
487
  use_cache=use_cache,
488
  cache_position=cache_position,
489
+ output_attentions=output_attentions,
490
+ output_hidden_states=output_hidden_states,
491
  return_dict=True,
492
  **kwargs,
493
  )
 
522
  loss=loss,
523
  logits=logits,
524
  past_key_values=outputs.past_key_values,
525
+ hidden_states=outputs.hidden_states,
526
+ attentions=outputs.attentions,
527
  )
528
 
529
  def prepare_inputs_for_generation(
source/.pytest_cache/README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # pytest cache directory #
2
+
3
+ This directory contains data from the pytest's cache plugin,
4
+ which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
5
+
6
+ **Do not** commit this to version control.
7
+
8
+ See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
source/QYROU_ARCH_ARCHITECTURE_AUDIT.md CHANGED
@@ -15,6 +15,16 @@ attention masks, position IDs, and cache-related generation arguments.
15
  The recommended Qyrou-1 architecture is therefore the preview architecture with
16
  its training graph retained and its serving contract completed before release.
17
 
 
 
 
 
 
 
 
 
 
 
18
  ## Exact architecture
19
 
20
  | Component | Value |
@@ -31,6 +41,7 @@ its training graph retained and its serving contract completed before release.
31
  | FFN intermediate width | 1,328 |
32
  | FFN | SwiGLU |
33
  | Normalization | pre-RMSNorm |
 
34
  | Positional encoding | RoPE, theta 10,000 |
35
  | Context length | 2,048 |
36
  | Convolution | causal depthwise kernel 7 + dense pointwise projection |
@@ -51,15 +62,16 @@ retain global causal communication.
51
  | Component | Count |
52
  |---|---:|
53
  | Tied token embedding / LM head | 10,240,000 |
54
- | 17 attention blocks | 45,835,264 |
55
  | 4 convolution blocks | 9,226,240 |
56
  | Final RMSNorm | 512 |
57
- | **Total** | **65,302,016** |
58
 
59
- An attention block has 2,696,192 parameters:
60
 
61
  - packed QKV projection: 393,216
62
  - output projection: 262,144
 
63
  - SwiGLU projections: 2,039,808
64
  - two RMSNorm weights: 1,024
65
 
@@ -111,57 +123,72 @@ Positive properties:
111
 
112
  Required hardening:
113
 
114
- - validate that the packed last dimension is even;
115
- - validate CUDA placement and supported floating dtypes before launching;
116
- - add reference forward/backward parity tests for BF16 and FP32;
117
- - add odd/tail-width, non-contiguous input, zero-size, and large-row tests;
118
- - benchmark against Liger's existing SwiGLU and plain compiled PyTorch;
 
 
119
  - avoid importing `calculate_settings` from Liger inside the custom-kernel module
120
- if the goal is an independently loadable kernel;
 
121
  - document supported Triton and `triton-windows` versions rather than monkey
122
- patching dependency version checks at import time.
 
123
 
124
  ## Release blockers
125
 
126
- ### 1. No incremental generation cache
127
 
128
  `forward` accepts only `input_ids` as an effective model input. Cache-related
129
  arguments are swallowed and ignored. Every generated token therefore recomputes
130
  all preceding attention, convolution, FFN, and logits work.
131
 
132
- Implement:
133
 
134
  - per-layer K/V cache for all 17 attention layers;
135
  - six-token rolling state for each kernel-7 convolution layer;
136
  - cache position and RoPE offset handling;
137
  - `past_key_values`, `use_cache`, and a cache-aware model output;
138
- - `prepare_inputs_for_generation` and cache reordering.
 
 
 
139
 
140
- ### 2. Attention masks are ignored
141
 
142
  Padded batched inference and packed examples can attend to padding or unrelated
143
- tokens. Accept and correctly combine `attention_mask` with causality. Add tests
144
- for left padding, right padding, unequal prompt lengths, and fully unpadded
145
- equivalence.
 
 
 
146
 
147
- ### 3. Position IDs and RoPE offsets are ignored
148
 
149
  RoPE always begins at zero and is keyed only by sequence length, device, and
150
- dtype. Incremental decoding and padded batches need explicit positions. Cache
151
- cosine/sine tables by capacity, then gather using `position_ids`.
 
 
152
 
153
- ### 4. Serving outputs do not follow the full Transformers contract
154
 
155
- Add support for `return_dict`, hidden states when requested, cache outputs, and
156
- the standard causal-LM generation inputs. Reject unsupported arguments rather
157
- than silently swallowing every keyword.
 
 
158
 
159
- ### 5. Context policy is fixed at 2,048
160
 
161
  The architecture can run longer, but it was trained at 2,048 with base RoPE.
162
  Do not advertise longer context without continued pretraining and long-context
163
  evaluation. If longer context is a Qyrou-1 goal, train it deliberately rather
164
- than changing `max_position_embeddings` after the fact.
 
165
 
166
  ## Data and training findings
167
 
@@ -195,16 +222,22 @@ completed 10B-token preview experiment.
195
  Complete the model in this order:
196
 
197
  1. Establish PyTorch reference tests for attention, convolution, RMSNorm, packed
198
- SwiGLU, loss shifting, and tied weights.
199
- 2. Add the complete cache and attention-mask serving contract.
200
- 3. Add Triton parity and gradient tests before further fusion.
201
- 4. Benchmark packed GEMMs, SwiGLU, RMSNorm, SDPA, and cross entropy separately.
 
 
 
 
202
  5. Add optional optimized paths behind capability checks, preserving a plain
203
- PyTorch fallback.
204
- 6. Run a short continued-pretraining smoke test from the preview checkpoint.
205
- 7. Evaluate loss/perplexity plus uncontaminated downstream tasks.
 
 
206
  8. Only then consider additional fused residual+RMSNorm or convolution kernels;
207
- optimize measured bottlenecks, not launch count in isolation.
208
 
209
  The central design decision is sound: a 512-wide, 21-layer hybrid with 2-KV-head
210
  GQA and four causal-convolution mixers gives Qyrou-1 a distinct architecture
 
15
  The recommended Qyrou-1 architecture is therefore the preview architecture with
16
  its training graph retained and its serving contract completed before release.
17
 
18
+ **Status update (release pass):** the serving contract is now complete. The model
19
+ maintains a hybrid incremental cache (per-layer attention K/V plus a rolling
20
+ six-token convolution state), honors `attention_mask` in combination with
21
+ causality, applies RoPE at explicit `position_ids`, returns hidden states and
22
+ attention weights on request, and rejects unsupported arguments. The full CPU
23
+ test suite passes (30 tests plus 8 CUDA-gated Triton tests), including cached
24
+ versus uncached equivalence for left-padded, right-padded, and unequal-length
25
+ batches. The remaining open item is the context policy (blocker 5), which is a
26
+ training decision rather than an implementation gap.
27
+
28
  ## Exact architecture
29
 
30
  | Component | Value |
 
41
  | FFN intermediate width | 1,328 |
42
  | FFN | SwiGLU |
43
  | Normalization | pre-RMSNorm |
44
+ | Query/key states | QK-norm (per-head RMSNorm on query and key states) |
45
  | Positional encoding | RoPE, theta 10,000 |
46
  | Context length | 2,048 |
47
  | Convolution | causal depthwise kernel 7 + dense pointwise projection |
 
62
  | Component | Count |
63
  |---|---:|
64
  | Tied token embedding / LM head | 10,240,000 |
65
+ | 17 attention blocks | 45,837,440 |
66
  | 4 convolution blocks | 9,226,240 |
67
  | Final RMSNorm | 512 |
68
+ | **Total** | **65,304,192** |
69
 
70
+ An attention block has 2,696,320 parameters:
71
 
72
  - packed QKV projection: 393,216
73
  - output projection: 262,144
74
+ - QK-norm weights (query and key heads): 128
75
  - SwiGLU projections: 2,039,808
76
  - two RMSNorm weights: 1,024
77
 
 
123
 
124
  Required hardening:
125
 
126
+ - validate that the packed last dimension is even — **done** (`PackedSwiGLUFunction.forward`);
127
+ - validate CUDA placement and supported floating dtypes before launching — **done**;
128
+ - add reference forward/backward parity tests for BF16 and FP32 — **done**
129
+ (`tests/test_triton_kernel.py`);
130
+ - add odd/tail-width, non-contiguous input, zero-size, and large-row tests — **done**;
131
+ - benchmark against Liger's existing SwiGLU and plain compiled PyTorch — **done**
132
+ (`scripts/benchmark_micro.py`);
133
  - avoid importing `calculate_settings` from Liger inside the custom-kernel module
134
+ if the goal is an independently loadable kernel — **done**; the kernel module has
135
+ no Liger import;
136
  - document supported Triton and `triton-windows` versions rather than monkey
137
+ patching dependency version checks at import time — open; the kernel module
138
+ loads `triton` directly and relies on the environment pinning.
139
 
140
  ## Release blockers
141
 
142
+ ### 1. No incremental generation cache — resolved
143
 
144
  `forward` accepts only `input_ids` as an effective model input. Cache-related
145
  arguments are swallowed and ignored. Every generated token therefore recomputes
146
  all preceding attention, convolution, FFN, and logits work.
147
 
148
+ Implemented:
149
 
150
  - per-layer K/V cache for all 17 attention layers;
151
  - six-token rolling state for each kernel-7 convolution layer;
152
  - cache position and RoPE offset handling;
153
  - `past_key_values`, `use_cache`, and a cache-aware model output;
154
+ - `prepare_inputs_for_generation` and cache reordering;
155
+ - framework cache interop: a `DynamicCache` supplied by the Transformers
156
+ generation loop is converted into the hybrid cache on entry, and the hybrid
157
+ cache is returned for all subsequent decode steps.
158
 
159
+ ### 2. Attention masks are ignored — resolved
160
 
161
  Padded batched inference and packed examples can attend to padding or unrelated
162
+ tokens. The model now accepts `attention_mask` and combines it with causality.
163
+ Tests cover left padding, right padding, unequal prompt lengths, and fully
164
+ unpadded equivalence (`tests/test_model.py`). Cached and uncached forward passes
165
+ are numerically identical for all of these shapes, which required the convolution
166
+ state to store the last *positions* (including padding) rather than the last
167
+ *valid* tokens.
168
 
169
+ ### 3. Position IDs and RoPE offsets are ignored — resolved
170
 
171
  RoPE always begins at zero and is keyed only by sequence length, device, and
172
+ dtype. The model now precomputes cosine/sine tables for the full context and
173
+ gathers them with explicit `position_ids`, so incremental decoding and padded
174
+ batches receive correct offsets. Exceeding the configured context raises a clear
175
+ error rather than silently wrapping.
176
 
177
+ ### 4. Serving outputs do not follow the full Transformers contract — resolved
178
 
179
+ The model supports `return_dict`, hidden states when requested, attention weights
180
+ when requested, cache outputs, and the standard causal-LM generation inputs.
181
+ Unknown keyword arguments are rejected with an explicit error. Generation through
182
+ `GenerationMixin` is verified end to end and matches a cache-disabled pass
183
+ token-for-token.
184
 
185
+ ### 5. Context policy is fixed at 2,048 — open, by design
186
 
187
  The architecture can run longer, but it was trained at 2,048 with base RoPE.
188
  Do not advertise longer context without continued pretraining and long-context
189
  evaluation. If longer context is a Qyrou-1 goal, train it deliberately rather
190
+ than changing `max_position_embeddings` after the fact. The implementation
191
+ enforces the configured limit.
192
 
193
  ## Data and training findings
194
 
 
222
  Complete the model in this order:
223
 
224
  1. Establish PyTorch reference tests for attention, convolution, RMSNorm, packed
225
+ SwiGLU, loss shifting, and tied weights — **done**
226
+ (`tests/test_reference.py`; 8 reference tests, all passing).
227
+ 2. Add the complete cache and attention-mask serving contract **done**
228
+ (see the release-blocker section above; 30 CPU tests passing).
229
+ 3. Add Triton parity and gradient tests before further fusion — **done**
230
+ (8 CUDA-gated tests in `tests/test_triton_kernel.py`; executable on GPU).
231
+ 4. Benchmark packed GEMMs, SwiGLU, RMSNorm, SDPA, and cross entropy separately —
232
+ **done** (`scripts/benchmark_micro.py`, CUDA required).
233
  5. Add optional optimized paths behind capability checks, preserving a plain
234
+ PyTorch fallback — **done** (Liger RMSNorm/SiLU-Mul, Triton packed SwiGLU, and
235
+ cut-cross-entropy all fall back to eager PyTorch when unavailable).
236
+ 6. Run a short continued-pretraining smoke test from the preview checkpoint —
237
+ open.
238
+ 7. Evaluate loss/perplexity plus uncontaminated downstream tasks — open.
239
  8. Only then consider additional fused residual+RMSNorm or convolution kernels;
240
+ optimize measured bottlenecks, not launch count in isolation — open.
241
 
242
  The central design decision is sound: a 512-wide, 21-layer hybrid with 2-KV-head
243
  GQA and four causal-convolution mixers gives Qyrou-1 a distinct architecture
source/qyrou_arch/modeling_qyrou_arch.py CHANGED
@@ -13,6 +13,19 @@ from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutpu
13
  from .cache import QyrouArchHybridCache
14
  from .configuration_qyrou_arch import QyrouArchConfig
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  try:
17
  from .triton_kernels import PackedSwiGLUFunction
18
  except (ImportError, RuntimeError):
@@ -60,27 +73,6 @@ def _apply_rope(
60
  return torch.stack((even * cos - odd * sin, even * sin + odd * cos), dim=-1).flatten(-2)
61
 
62
 
63
- def _last_valid_state(
64
- x: torch.Tensor,
65
- mask: torch.Tensor | None,
66
- state_length: int,
67
- ) -> torch.Tensor:
68
- if state_length == 0:
69
- return x[:, :0]
70
- if mask is None:
71
- return F.pad(x, (0, 0, max(0, state_length - x.shape[1]), 0))[:, -state_length:]
72
- result = torch.zeros(
73
- (x.shape[0], state_length, x.shape[2]),
74
- dtype=x.dtype,
75
- device=x.device,
76
- )
77
- for batch_index in range(x.shape[0]):
78
- valid = x[batch_index][mask[batch_index].bool()]
79
- valid = valid[-state_length:]
80
- result[batch_index, -valid.shape[0] :] = valid
81
- return result
82
-
83
-
84
  class CausalGQA(nn.Module):
85
  def __init__(self, config: QyrouArchConfig, layer_idx: int) -> None:
86
  super().__init__()
@@ -109,7 +101,8 @@ class CausalGQA(nn.Module):
109
  attention_mask: torch.Tensor | None,
110
  cache: QyrouArchHybridCache | None,
111
  cache_position: torch.Tensor,
112
- ) -> torch.Tensor:
 
113
  batch, query_length, _ = x.shape
114
  if hasattr(self, "qkv_proj"):
115
  q, k, v = self.qkv_proj(x).split((self.q_size, self.kv_size, self.kv_size), dim=-1)
@@ -135,7 +128,22 @@ class CausalGQA(nn.Module):
135
  if attention_mask.shape[-1] < key_length:
136
  raise ValueError("attention_mask is shorter than the cached key sequence")
137
  mask = mask & attention_mask[:, None, None, :key_length].bool()
138
- if q.is_cuda and self.attention_backend in {"cudnn", "flash"}:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  backends = (
140
  [SDPBackend.CUDNN_ATTENTION, SDPBackend.FLASH_ATTENTION, SDPBackend.MATH]
141
  if self.attention_backend == "cudnn"
@@ -163,7 +171,7 @@ class CausalGQA(nn.Module):
163
  enable_gqa=True,
164
  )
165
  output = output.transpose(1, 2).contiguous().view(batch, query_length, -1)
166
- return self.o_proj(output)
167
 
168
 
169
  class CausalConvMixer(nn.Module):
@@ -187,8 +195,9 @@ class CausalConvMixer(nn.Module):
187
  _rope: tuple[torch.Tensor, torch.Tensor],
188
  attention_mask: torch.Tensor | None,
189
  cache: QyrouArchHybridCache | None,
190
- _cache_position: torch.Tensor,
191
- ) -> torch.Tensor:
 
192
  state_length = self.kernel_size - 1
193
  query_mask = attention_mask[:, -x.shape[1] :] if attention_mask is not None else None
194
  if query_mask is not None:
@@ -205,24 +214,9 @@ class CausalConvMixer(nn.Module):
205
  )
206
  combined = torch.cat((previous, x), dim=1)
207
  conv_input = combined.transpose(1, 2)
208
- combined_mask = None
209
- if query_mask is not None:
210
- combined_mask = torch.cat(
211
- (
212
- torch.ones(
213
- (x.shape[0], state_length),
214
- dtype=query_mask.dtype,
215
- device=query_mask.device,
216
- ),
217
- query_mask,
218
- ),
219
- dim=1,
220
- )
221
- cache.update_convolution(
222
- self.layer_idx,
223
- _last_valid_state(combined, combined_mask, state_length),
224
- )
225
- return self.pointwise(self.depthwise(conv_input).transpose(1, 2))
226
 
227
 
228
  class SwiGLU(nn.Module):
@@ -280,9 +274,39 @@ class QyrouArchBlock(nn.Module):
280
  attention_mask: torch.Tensor | None,
281
  cache: QyrouArchHybridCache | None,
282
  cache_position: torch.Tensor,
283
- ) -> torch.Tensor:
284
- x = x + self.mixer(self.mixer_norm(x), rope, attention_mask, cache, cache_position)
285
- return x + self.ffn(self.ffn_norm(x))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
 
288
  class QyrouArchPreTrainedModel(PreTrainedModel):
@@ -342,9 +366,14 @@ class QyrouArchModel(QyrouArchPreTrainedModel):
342
  inputs_embeds: torch.Tensor | None = None,
343
  use_cache: bool | None = None,
344
  cache_position: torch.LongTensor | None = None,
 
 
345
  return_dict: bool | None = None,
346
- **_: Any,
347
  ) -> BaseModelOutputWithPast | tuple[torch.Tensor, QyrouArchHybridCache | None]:
 
 
 
348
  if (input_ids is None) == (inputs_embeds is None):
349
  raise ValueError("Pass exactly one of input_ids or inputs_embeds")
350
  hidden = self.embed_tokens(input_ids) if inputs_embeds is None else inputs_embeds
@@ -353,8 +382,18 @@ class QyrouArchModel(QyrouArchPreTrainedModel):
353
  batch, query_length, _ = hidden.shape
354
  use_cache = self.config.use_cache if use_cache is None else use_cache
355
  return_dict = self.config.return_dict if return_dict is None else return_dict
 
 
 
 
 
 
 
 
356
  if use_cache and past_key_values is None:
357
  past_key_values = QyrouArchHybridCache(self.config.num_hidden_layers)
 
 
358
  cache = past_key_values if use_cache else None
359
  past_length = cache.get_seq_length() if cache is not None else 0
360
  if cache_position is None:
@@ -369,14 +408,32 @@ class QyrouArchModel(QyrouArchPreTrainedModel):
369
  else:
370
  position_ids = cache_position.unsqueeze(0).expand(batch, -1)
371
  rope = self._rope(position_ids, hidden.dtype)
 
 
372
  for layer in self.layers:
373
- hidden = layer(hidden, rope, attention_mask, cache, cache_position)
 
 
 
 
 
 
 
 
 
 
 
374
  hidden = self.norm(hidden)
375
  if cache is not None:
376
  cache.finish_step(cache_position)
377
  if not return_dict:
378
  return hidden, cache
379
- return BaseModelOutputWithPast(last_hidden_state=hidden, past_key_values=cache)
 
 
 
 
 
380
 
381
 
382
  class QyrouArchForCausalLM(QyrouArchPreTrainedModel, GenerationMixin):
@@ -411,10 +468,15 @@ class QyrouArchForCausalLM(QyrouArchPreTrainedModel, GenerationMixin):
411
  labels: torch.LongTensor | None = None,
412
  use_cache: bool | None = None,
413
  cache_position: torch.LongTensor | None = None,
 
 
414
  return_logits: bool = True,
415
  return_dict: bool | None = None,
416
  **kwargs: Any,
417
  ) -> CausalLMOutputWithPast | tuple[Any, ...]:
 
 
 
418
  return_dict = self.config.return_dict if return_dict is None else return_dict
419
  outputs = self.model(
420
  input_ids=input_ids,
@@ -424,6 +486,8 @@ class QyrouArchForCausalLM(QyrouArchPreTrainedModel, GenerationMixin):
424
  inputs_embeds=inputs_embeds,
425
  use_cache=use_cache,
426
  cache_position=cache_position,
 
 
427
  return_dict=True,
428
  **kwargs,
429
  )
@@ -458,6 +522,8 @@ class QyrouArchForCausalLM(QyrouArchPreTrainedModel, GenerationMixin):
458
  loss=loss,
459
  logits=logits,
460
  past_key_values=outputs.past_key_values,
 
 
461
  )
462
 
463
  def prepare_inputs_for_generation(
 
13
  from .cache import QyrouArchHybridCache
14
  from .configuration_qyrou_arch import QyrouArchConfig
15
 
16
+ _FRAMEWORK_KWARGS = frozenset(
17
+ {
18
+ "num_items_in_batch",
19
+ "output_router_logits",
20
+ "cu_seq_lens_q",
21
+ "cu_seq_lens_k",
22
+ "max_length_q",
23
+ "max_length_k",
24
+ "is_causal",
25
+ "seq_idx",
26
+ }
27
+ )
28
+
29
  try:
30
  from .triton_kernels import PackedSwiGLUFunction
31
  except (ImportError, RuntimeError):
 
73
  return torch.stack((even * cos - odd * sin, even * sin + odd * cos), dim=-1).flatten(-2)
74
 
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  class CausalGQA(nn.Module):
77
  def __init__(self, config: QyrouArchConfig, layer_idx: int) -> None:
78
  super().__init__()
 
101
  attention_mask: torch.Tensor | None,
102
  cache: QyrouArchHybridCache | None,
103
  cache_position: torch.Tensor,
104
+ output_attentions: bool = False,
105
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
106
  batch, query_length, _ = x.shape
107
  if hasattr(self, "qkv_proj"):
108
  q, k, v = self.qkv_proj(x).split((self.q_size, self.kv_size, self.kv_size), dim=-1)
 
128
  if attention_mask.shape[-1] < key_length:
129
  raise ValueError("attention_mask is shorter than the cached key sequence")
130
  mask = mask & attention_mask[:, None, None, :key_length].bool()
131
+ if output_attentions:
132
+ if mask is None:
133
+ mask = torch.tril(
134
+ torch.ones((1, 1, query_length, key_length), dtype=torch.bool, device=x.device)
135
+ ).expand(batch, 1, query_length, key_length)
136
+ query_heads = q.shape[1]
137
+ kv_heads = k.shape[1]
138
+ if kv_heads != query_heads:
139
+ repeat = query_heads // kv_heads
140
+ k = k.repeat_interleave(repeat, dim=1)
141
+ v = v.repeat_interleave(repeat, dim=1)
142
+ scores = torch.matmul(q, k.transpose(-2, -1)) * (self.head_dim**-0.5)
143
+ scores = scores.masked_fill(~mask, torch.finfo(scores.dtype).min)
144
+ weights = torch.softmax(scores, dim=-1)
145
+ output = torch.matmul(weights, v)
146
+ elif q.is_cuda and self.attention_backend in {"cudnn", "flash"}:
147
  backends = (
148
  [SDPBackend.CUDNN_ATTENTION, SDPBackend.FLASH_ATTENTION, SDPBackend.MATH]
149
  if self.attention_backend == "cudnn"
 
171
  enable_gqa=True,
172
  )
173
  output = output.transpose(1, 2).contiguous().view(batch, query_length, -1)
174
+ return self.o_proj(output), weights if output_attentions else None
175
 
176
 
177
  class CausalConvMixer(nn.Module):
 
195
  _rope: tuple[torch.Tensor, torch.Tensor],
196
  attention_mask: torch.Tensor | None,
197
  cache: QyrouArchHybridCache | None,
198
+ cache_position: torch.Tensor,
199
+ _output_attentions: bool = False,
200
+ ) -> tuple[torch.Tensor, None]:
201
  state_length = self.kernel_size - 1
202
  query_mask = attention_mask[:, -x.shape[1] :] if attention_mask is not None else None
203
  if query_mask is not None:
 
214
  )
215
  combined = torch.cat((previous, x), dim=1)
216
  conv_input = combined.transpose(1, 2)
217
+ cache.update_convolution(self.layer_idx, combined[:, -state_length:])
218
+ output = self.pointwise(self.depthwise(conv_input).transpose(1, 2))
219
+ return output, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
 
222
  class SwiGLU(nn.Module):
 
274
  attention_mask: torch.Tensor | None,
275
  cache: QyrouArchHybridCache | None,
276
  cache_position: torch.Tensor,
277
+ output_attentions: bool = False,
278
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
279
+ mixer_output, weights = self.mixer(
280
+ self.mixer_norm(x),
281
+ rope,
282
+ attention_mask,
283
+ cache,
284
+ cache_position,
285
+ output_attentions,
286
+ )
287
+ x = x + mixer_output
288
+ return x + self.ffn(self.ffn_norm(x)), weights
289
+
290
+
291
+ def _as_hybrid_cache(past_key_values: Any, num_layers: int) -> QyrouArchHybridCache:
292
+ cache = QyrouArchHybridCache(num_layers)
293
+ layers = getattr(past_key_values, "layers", None)
294
+ if layers is not None:
295
+ for index, layer in enumerate(layers[:num_layers]):
296
+ if getattr(layer, "is_initialized", False):
297
+ cache.attention[index] = (layer.keys, layer.values)
298
+ else:
299
+ key_cache = getattr(past_key_values, "key_cache", None)
300
+ if key_cache is None:
301
+ raise TypeError(
302
+ "past_key_values must be a QyrouArchHybridCache or a framework Cache instance"
303
+ )
304
+ value_cache = past_key_values.value_cache
305
+ for index in range(min(len(key_cache), num_layers)):
306
+ if key_cache[index] is not None:
307
+ cache.attention[index] = (key_cache[index], value_cache[index])
308
+ cache.seen_tokens = past_key_values.get_seq_length()
309
+ return cache
310
 
311
 
312
  class QyrouArchPreTrainedModel(PreTrainedModel):
 
366
  inputs_embeds: torch.Tensor | None = None,
367
  use_cache: bool | None = None,
368
  cache_position: torch.LongTensor | None = None,
369
+ output_attentions: bool | None = None,
370
+ output_hidden_states: bool | None = None,
371
  return_dict: bool | None = None,
372
+ **kwargs: Any,
373
  ) -> BaseModelOutputWithPast | tuple[torch.Tensor, QyrouArchHybridCache | None]:
374
+ unsupported = set(kwargs) - _FRAMEWORK_KWARGS
375
+ if unsupported:
376
+ raise ValueError(f"Unsupported model arguments: {sorted(unsupported)}")
377
  if (input_ids is None) == (inputs_embeds is None):
378
  raise ValueError("Pass exactly one of input_ids or inputs_embeds")
379
  hidden = self.embed_tokens(input_ids) if inputs_embeds is None else inputs_embeds
 
382
  batch, query_length, _ = hidden.shape
383
  use_cache = self.config.use_cache if use_cache is None else use_cache
384
  return_dict = self.config.return_dict if return_dict is None else return_dict
385
+ output_attentions = bool(
386
+ getattr(self.config, "output_attentions", False) if output_attentions is None else output_attentions
387
+ )
388
+ output_hidden_states = bool(
389
+ getattr(self.config, "output_hidden_states", False)
390
+ if output_hidden_states is None
391
+ else output_hidden_states
392
+ )
393
  if use_cache and past_key_values is None:
394
  past_key_values = QyrouArchHybridCache(self.config.num_hidden_layers)
395
+ if past_key_values is not None and not isinstance(past_key_values, QyrouArchHybridCache):
396
+ past_key_values = _as_hybrid_cache(past_key_values, self.config.num_hidden_layers)
397
  cache = past_key_values if use_cache else None
398
  past_length = cache.get_seq_length() if cache is not None else 0
399
  if cache_position is None:
 
408
  else:
409
  position_ids = cache_position.unsqueeze(0).expand(batch, -1)
410
  rope = self._rope(position_ids, hidden.dtype)
411
+ hidden_states = (hidden,) if output_hidden_states else None
412
+ attentions = () if output_attentions else None
413
  for layer in self.layers:
414
+ hidden, weights = layer(
415
+ hidden,
416
+ rope,
417
+ attention_mask,
418
+ cache,
419
+ cache_position,
420
+ output_attentions,
421
+ )
422
+ if hidden_states is not None:
423
+ hidden_states += (hidden,)
424
+ if attentions is not None:
425
+ attentions += (weights,)
426
  hidden = self.norm(hidden)
427
  if cache is not None:
428
  cache.finish_step(cache_position)
429
  if not return_dict:
430
  return hidden, cache
431
+ return BaseModelOutputWithPast(
432
+ last_hidden_state=hidden,
433
+ past_key_values=cache,
434
+ hidden_states=hidden_states,
435
+ attentions=attentions,
436
+ )
437
 
438
 
439
  class QyrouArchForCausalLM(QyrouArchPreTrainedModel, GenerationMixin):
 
468
  labels: torch.LongTensor | None = None,
469
  use_cache: bool | None = None,
470
  cache_position: torch.LongTensor | None = None,
471
+ output_attentions: bool | None = None,
472
+ output_hidden_states: bool | None = None,
473
  return_logits: bool = True,
474
  return_dict: bool | None = None,
475
  **kwargs: Any,
476
  ) -> CausalLMOutputWithPast | tuple[Any, ...]:
477
+ unsupported = set(kwargs) - _FRAMEWORK_KWARGS
478
+ if unsupported:
479
+ raise ValueError(f"Unsupported model arguments: {sorted(unsupported)}")
480
  return_dict = self.config.return_dict if return_dict is None else return_dict
481
  outputs = self.model(
482
  input_ids=input_ids,
 
486
  inputs_embeds=inputs_embeds,
487
  use_cache=use_cache,
488
  cache_position=cache_position,
489
+ output_attentions=output_attentions,
490
+ output_hidden_states=output_hidden_states,
491
  return_dict=True,
492
  **kwargs,
493
  )
 
522
  loss=loss,
523
  logits=logits,
524
  past_key_values=outputs.past_key_values,
525
+ hidden_states=outputs.hidden_states,
526
+ attentions=outputs.attentions,
527
  )
528
 
529
  def prepare_inputs_for_generation(
source/scripts/benchmark_micro.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import time
6
+ from statistics import median
7
+
8
+ import torch
9
+
10
+
11
+ def timed(fn, warmup: int, steps: int) -> float:
12
+ for _ in range(warmup):
13
+ fn()
14
+ torch.cuda.synchronize()
15
+ timings = []
16
+ for _ in range(steps):
17
+ started = time.perf_counter()
18
+ fn()
19
+ torch.cuda.synchronize()
20
+ timings.append(time.perf_counter() - started)
21
+ return median(timings)
22
+
23
+
24
+ def benchmark_cross_entropy(head_dim: int, vocab: int, rows: int) -> dict[str, float]:
25
+ hidden = torch.randn(rows, head_dim, device="cuda", dtype=torch.bfloat16)
26
+ weight = torch.randn(vocab, head_dim, device="cuda", dtype=torch.bfloat16)
27
+ labels = torch.randint(0, vocab, (rows,), device="cuda")
28
+ torch.backends.cuda.matmul.allow_tf32 = True
29
+ standard_ms = timed(lambda: torch.nn.functional.cross_entropy(
30
+ hidden @ weight.t(),
31
+ labels,
32
+ ), 3, 10) * 1e3
33
+ result = {"standard_ms": standard_ms}
34
+ try:
35
+ from cut_cross_entropy import linear_cross_entropy
36
+
37
+ cut_ms = timed(lambda: linear_cross_entropy(
38
+ hidden,
39
+ weight,
40
+ labels,
41
+ shift=False,
42
+ ), 3, 10) * 1e3
43
+ result["cut_ms"] = cut_ms
44
+ except ImportError:
45
+ result["cut_ms"] = None
46
+ return result
47
+
48
+
49
+ def benchmark_swiglu(features: int, rows: int) -> dict[str, float]:
50
+ packed = torch.randn(rows, 2 * features, device="cuda", dtype=torch.bfloat16)
51
+ eager_ms = timed(lambda: torch.nn.functional.silu(packed[..., :features]) * packed[..., features:], 3, 10) * 1e3
52
+ result = {"eager_ms": eager_ms}
53
+ try:
54
+ from qyrou_arch.triton_kernels import PackedSwiGLUFunction
55
+
56
+ triton_ms = timed(lambda: PackedSwiGLUFunction.apply(packed), 3, 10) * 1e3
57
+ result["triton_ms"] = triton_ms
58
+ except (ImportError, RuntimeError):
59
+ result["triton_ms"] = None
60
+ try:
61
+ from liger_kernel.ops.swiglu import LigerSiLUMulFunction
62
+
63
+ liger_ms = timed(
64
+ lambda: LigerSiLUMulFunction.apply(packed[..., :features], packed[..., features:], 1.0, 1.0),
65
+ 3,
66
+ 10,
67
+ ) * 1e3
68
+ result["liger_ms"] = liger_ms
69
+ except ImportError:
70
+ result["liger_ms"] = None
71
+ return result
72
+
73
+
74
+ def benchmark_rmsnorm(features: int, rows: int) -> dict[str, float]:
75
+ x = torch.randn(rows, features, device="cuda", dtype=torch.bfloat16)
76
+ weight = torch.ones(features, device="cuda", dtype=torch.bfloat16)
77
+
78
+ def eager() -> None:
79
+ normalized = x.float() * torch.rsqrt(x.float().square().mean(-1, keepdim=True) + 1e-5)
80
+ _ = normalized.to(x.dtype) * weight.to(x.dtype)
81
+
82
+ result = {"eager_ms": timed(eager, 3, 10) * 1e3}
83
+ try:
84
+ from liger_kernel.ops.rms_norm import LigerRMSNormFunction
85
+
86
+ liger_ms = timed(
87
+ lambda: LigerRMSNormFunction.apply(x, weight, 1e-5, 0.0, "llama", False, None),
88
+ 3,
89
+ 10,
90
+ ) * 1e3
91
+ result["liger_ms"] = liger_ms
92
+ except ImportError:
93
+ result["liger_ms"] = None
94
+ return result
95
+
96
+
97
+ def benchmark_sdpa(heads: int, seq: int, head_dim: int, batch: int) -> dict[str, float]:
98
+ import torch.nn.functional as functional
99
+ from torch.nn.attention import SDPBackend, sdpa_kernel
100
+
101
+ q = torch.randn(batch, heads, seq, head_dim, device="cuda", dtype=torch.bfloat16)
102
+ k = torch.randn(batch, heads, seq, head_dim, device="cuda", dtype=torch.bfloat16)
103
+ v = torch.randn(batch, heads, seq, head_dim, device="cuda", dtype=torch.bfloat16)
104
+ result = {}
105
+ for backend in ("MATH", "FLASH_ATTENTION", "CUDNN_ATTENTION"):
106
+ backend_flag = getattr(SDPBackend, backend)
107
+ try:
108
+ with sdpa_kernel([backend_flag], set_priority=True):
109
+ measured = timed(
110
+ lambda: functional.scaled_dot_product_attention(q, k, v, is_causal=True),
111
+ 3,
112
+ 10,
113
+ ) * 1e3
114
+ result[backend.lower()] = measured
115
+ except Exception:
116
+ result[backend.lower()] = None
117
+ return result
118
+
119
+
120
+ def benchmark_gemm(m: int, n: int, k: int) -> float:
121
+ left = torch.randn(m, k, device="cuda", dtype=torch.bfloat16)
122
+ right = torch.randn(k, n, device="cuda", dtype=torch.bfloat16)
123
+ torch.backends.cuda.matmul.allow_tf32 = True
124
+ return timed(lambda: left @ right, 3, 10) * 1e3
125
+
126
+
127
+ def main() -> None:
128
+ parser = argparse.ArgumentParser()
129
+ parser.add_argument("--rows", type=int, default=1024)
130
+ parser.add_argument("--steps", type=int, default=10)
131
+ args = parser.parse_args()
132
+ if not torch.cuda.is_available():
133
+ raise RuntimeError("Micro-benchmarks require CUDA")
134
+ torch.set_float32_matmul_precision("high")
135
+ torch.backends.cuda.matmul.allow_tf32 = True
136
+
137
+ report = {
138
+ "device": torch.cuda.get_device_name(0),
139
+ "compute_capability": torch.cuda.get_device_capability(0),
140
+ "qkv_gemm_ms": benchmark_gemm(2048, 768, 512),
141
+ "gate_up_gemm_ms": benchmark_gemm(2048, 2656, 512),
142
+ "down_gemm_ms": benchmark_gemm(2048, 512, 1328),
143
+ "swiglu": benchmark_swiglu(1328, args.rows),
144
+ "rmsnorm": benchmark_rmsnorm(512, args.rows),
145
+ "sdpa": benchmark_sdpa(8, 2048, 64, 1),
146
+ "cross_entropy": benchmark_cross_entropy(512, 20000, args.rows),
147
+ }
148
+ print(json.dumps(report, indent=2))
149
+
150
+
151
+ if __name__ == "__main__":
152
+ main()
source/tests/test_model.py CHANGED
@@ -96,3 +96,79 @@ def test_left_padded_cached_batch() -> None:
96
  ).logits[:, -1]
97
  torch.testing.assert_close(cached, full, atol=2e-5, rtol=2e-4)
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  ).logits[:, -1]
97
  torch.testing.assert_close(cached, full, atol=2e-5, rtol=2e-4)
98
 
99
+
100
+ def test_right_padded_cached_batch() -> None:
101
+ torch.manual_seed(10)
102
+ model = QyrouArchForCausalLM(tiny_config()).eval()
103
+ tokens = torch.tensor([[10, 11, 12, 2, 2], [20, 21, 22, 23, 24]])
104
+ mask = tokens.ne(2).long()
105
+ cache = QyrouArchHybridCache(model.config.num_hidden_layers)
106
+ with torch.no_grad():
107
+ model(tokens, attention_mask=mask, past_key_values=cache, use_cache=True)
108
+ next_tokens = torch.tensor([[13], [25]])
109
+ extended_mask = torch.cat((mask, torch.ones((2, 1), dtype=mask.dtype)), dim=1)
110
+ cached = model(
111
+ next_tokens,
112
+ attention_mask=extended_mask,
113
+ past_key_values=cache,
114
+ cache_position=torch.tensor([5]),
115
+ use_cache=True,
116
+ ).logits[:, -1]
117
+ full = model(
118
+ torch.cat((tokens, next_tokens), dim=1),
119
+ attention_mask=extended_mask,
120
+ use_cache=False,
121
+ ).logits[:, -1]
122
+ torch.testing.assert_close(cached, full, atol=2e-5, rtol=2e-4)
123
+
124
+
125
+ def test_unequal_prompt_lengths_match_full_forward() -> None:
126
+ torch.manual_seed(11)
127
+ model = QyrouArchForCausalLM(tiny_config()).eval()
128
+ tokens = torch.tensor([[2, 2, 10, 11, 12, 13], [20, 21, 22, 2, 2, 2]])
129
+ mask = tokens.ne(2).long()
130
+ cache = QyrouArchHybridCache(model.config.num_hidden_layers)
131
+ with torch.no_grad():
132
+ model(tokens, attention_mask=mask, past_key_values=cache, use_cache=True)
133
+ next_tokens = torch.tensor([[14], [23]])
134
+ extended_mask = torch.cat((mask, torch.ones((2, 1), dtype=mask.dtype)), dim=1)
135
+ cached = model(
136
+ next_tokens,
137
+ attention_mask=extended_mask,
138
+ past_key_values=cache,
139
+ cache_position=torch.tensor([6]),
140
+ use_cache=True,
141
+ ).logits[:, -1]
142
+ full = model(
143
+ torch.cat((tokens, next_tokens), dim=1),
144
+ attention_mask=extended_mask,
145
+ use_cache=False,
146
+ ).logits[:, -1]
147
+ torch.testing.assert_close(cached, full, atol=2e-5, rtol=2e-4)
148
+
149
+
150
+ def test_unsupported_arguments_are_rejected() -> None:
151
+ model = QyrouArchForCausalLM(tiny_config()).eval()
152
+ tokens = torch.randint(5, 128, (1, 8))
153
+ with pytest.raises(ValueError, match="Unsupported model arguments"):
154
+ model(tokens, use_cache=False, made_up_argument=True)
155
+
156
+
157
+ def test_generation_with_framework_default_cache_matches_no_cache() -> None:
158
+ torch.manual_seed(12)
159
+ model = QyrouArchForCausalLM(tiny_config()).eval()
160
+ prompt = torch.tensor([[10, 11, 12, 13]])
161
+ kwargs = {"max_new_tokens": 8, "do_sample": False, "pad_token_id": 2, "eos_token_id": 1}
162
+ with torch.no_grad():
163
+ cached = model.generate(prompt, use_cache=True, **kwargs)
164
+ uncached = model.generate(prompt, use_cache=False, **kwargs)
165
+ assert cached.shape[1] == 12
166
+ assert torch.equal(cached, uncached)
167
+
168
+
169
+ def test_generation_rejects_unknown_cache_type() -> None:
170
+ model = QyrouArchForCausalLM(tiny_config()).eval()
171
+ tokens = torch.randint(5, 128, (1, 8))
172
+ with pytest.raises(TypeError, match="framework Cache"):
173
+ model(tokens, past_key_values=object(), use_cache=True)
174
+
source/tests/test_reference.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ torch = pytest.importorskip("torch")
6
+ pytest.importorskip("transformers")
7
+
8
+ from qyrou_arch.configuration_qyrou_arch import QyrouArchConfig
9
+ from qyrou_arch.modeling_qyrou_arch import (
10
+ CausalConvMixer,
11
+ CausalGQA,
12
+ RMSNorm,
13
+ SwiGLU,
14
+ QyrouArchForCausalLM,
15
+ _apply_rope,
16
+ )
17
+
18
+ torch.manual_seed(3)
19
+
20
+
21
+ def reference_config(**overrides) -> QyrouArchConfig:
22
+ values = dict(
23
+ vocab_size=128,
24
+ hidden_size=64,
25
+ num_hidden_layers=4,
26
+ num_attention_heads=4,
27
+ num_key_value_heads=2,
28
+ intermediate_size=128,
29
+ conv_layers=[2],
30
+ conv_kernel_size=3,
31
+ max_position_embeddings=32,
32
+ qk_norm=False,
33
+ liger_rms_norm=False,
34
+ liger_swiglu=False,
35
+ cut_cross_entropy=False,
36
+ attention_backend="math",
37
+ fused_qkv_projection=True,
38
+ fused_gate_up_projection=True,
39
+ )
40
+ values.update(overrides)
41
+ return QyrouArchConfig(**values)
42
+
43
+
44
+ def test_rmsnorm_matches_manual_reference() -> None:
45
+ module = RMSNorm(64, eps=1e-5)
46
+ inputs = torch.randn(3, 9, 64)
47
+ expected = (
48
+ inputs.float() * torch.rsqrt(inputs.float().square().mean(-1, keepdim=True) + 1e-5)
49
+ ) * module.weight
50
+ torch.testing.assert_close(module(inputs), expected, rtol=1e-5, atol=1e-5)
51
+
52
+
53
+ def test_rope_matches_manual_reference() -> None:
54
+ inputs = torch.randn(2, 5, 8)
55
+ positions = torch.tensor([3, 7, 11, 15, 19], dtype=torch.float32)
56
+ inv_freq = 1.0 / (10000.0 ** (torch.arange(0, 8, 2).float() / 8))
57
+ angles = torch.outer(positions, inv_freq)
58
+ cos, sin = angles.cos(), angles.sin()
59
+ even, odd = inputs[..., 0::2], inputs[..., 1::2]
60
+ expected = torch.stack(
61
+ (even * cos - odd * sin, even * sin + odd * cos), dim=-1
62
+ ).flatten(-2)
63
+ torch.testing.assert_close(
64
+ _apply_rope(inputs, cos.unsqueeze(0), sin.unsqueeze(0)),
65
+ expected,
66
+ rtol=1e-5,
67
+ atol=1e-5,
68
+ )
69
+
70
+
71
+ def test_attention_matches_manual_reference() -> None:
72
+ config = reference_config()
73
+ layer = CausalGQA(config, layer_idx=0).eval()
74
+ inputs = torch.randn(2, 7, 64)
75
+ positions = torch.arange(7, dtype=torch.float32)
76
+ inv_freq = 1.0 / (config.rope_theta ** (torch.arange(0, 16, 2).float() / 16))
77
+ angles = torch.outer(positions, inv_freq)
78
+ cos = angles.cos().unsqueeze(0).unsqueeze(1)
79
+ sin = angles.sin().unsqueeze(0).unsqueeze(1)
80
+
81
+ qkv = layer.qkv_proj(inputs)
82
+ q, k, v = qkv.split((layer.q_size, layer.kv_size, layer.kv_size), dim=-1)
83
+ q = q.view(2, 7, 4, 16).transpose(1, 2)
84
+ k = k.view(2, 7, 2, 16).transpose(1, 2)
85
+ v = v.view(2, 7, 2, 16).transpose(1, 2)
86
+ q = _apply_rope(q, cos, sin)
87
+ k = _apply_rope(k, cos, sin)
88
+ k = k.repeat_interleave(2, dim=1)
89
+ v = v.repeat_interleave(2, dim=1)
90
+ scores = torch.matmul(q, k.transpose(-2, -1)) / 4.0
91
+ mask = torch.tril(torch.ones((1, 1, 7, 7), dtype=torch.bool))
92
+ scores = scores.masked_fill(~mask, float("-inf"))
93
+ weights = torch.softmax(scores, dim=-1)
94
+ expected = torch.matmul(weights, v).transpose(1, 2).contiguous().view(2, 7, -1)
95
+ expected = layer.o_proj(expected)
96
+
97
+ with torch.no_grad():
98
+ actual, _ = layer(
99
+ inputs,
100
+ (cos, sin),
101
+ attention_mask=None,
102
+ cache=None,
103
+ cache_position=torch.arange(7),
104
+ )
105
+ torch.testing.assert_close(actual, expected, rtol=1e-4, atol=1e-4)
106
+
107
+
108
+ def test_convolution_matches_manual_reference() -> None:
109
+ config = reference_config()
110
+ layer = CausalConvMixer(config, layer_idx=2).eval()
111
+ inputs = torch.randn(2, 9, 64)
112
+ padded = torch.nn.functional.pad(inputs.transpose(1, 2), (2, 0))
113
+ expected = layer.pointwise(layer.depthwise(padded).transpose(1, 2))
114
+
115
+ with torch.no_grad():
116
+ actual, _ = layer(
117
+ inputs,
118
+ (None, None),
119
+ attention_mask=None,
120
+ cache=None,
121
+ cache_position=torch.arange(9),
122
+ )
123
+ torch.testing.assert_close(actual, expected, rtol=1e-5, atol=1e-5)
124
+
125
+
126
+ def test_swiglu_matches_manual_reference() -> None:
127
+ config = reference_config()
128
+ module = SwiGLU(config).eval()
129
+ inputs = torch.randn(2, 9, 64)
130
+ packed = module.gate_up_proj(inputs)
131
+ gate, up = packed.chunk(2, dim=-1)
132
+ expected = module.down_proj(torch.nn.functional.silu(gate) * up)
133
+
134
+ with torch.no_grad():
135
+ actual = module(inputs)
136
+ torch.testing.assert_close(actual, expected, rtol=1e-5, atol=1e-5)
137
+
138
+
139
+ def test_shifted_loss_matches_manual_cross_entropy() -> None:
140
+ model = QyrouArchForCausalLM(reference_config()).eval()
141
+ tokens = torch.randint(5, 128, (2, 8))
142
+ with torch.no_grad():
143
+ output = model(tokens, labels=tokens, return_logits=True, use_cache=False)
144
+ logits = output.logits[:, :-1].float().reshape(-1, 128)
145
+ targets = tokens[:, 1:].contiguous().view(-1)
146
+ expected = torch.nn.functional.cross_entropy(logits, targets)
147
+ torch.testing.assert_close(output.loss, expected, rtol=1e-4, atol=1e-4)
148
+
149
+
150
+ def test_attention_weights_sum_to_one_when_requested() -> None:
151
+ model = QyrouArchForCausalLM(reference_config()).eval()
152
+ tokens = torch.randint(5, 128, (1, 6))
153
+ with torch.no_grad():
154
+ output = model(tokens, use_cache=False, output_attentions=True)
155
+ assert output.attentions is not None
156
+ assert len(output.attentions) == 4
157
+ attention_weights = [weights for weights in output.attentions if weights is not None]
158
+ assert len(attention_weights) == 3
159
+ for weights in attention_weights:
160
+ assert weights.shape[1] == 4
161
+ torch.testing.assert_close(
162
+ weights.sum(dim=-1),
163
+ torch.ones_like(weights.sum(dim=-1)),
164
+ rtol=1e-4,
165
+ atol=1e-4,
166
+ )
167
+
168
+
169
+ def test_hidden_states_include_embeddings_and_every_layer() -> None:
170
+ model = QyrouArchForCausalLM(reference_config()).eval()
171
+ tokens = torch.randint(5, 128, (1, 6))
172
+ with torch.no_grad():
173
+ output = model(tokens, use_cache=False, output_hidden_states=True)
174
+ assert output.hidden_states is not None
175
+ assert len(output.hidden_states) == 5
176
+ assert output.hidden_states[0].shape == (1, 6, 64)
177
+ assert output.hidden_states[-1].shape == (1, 6, 64)
178
+ for hidden in output.hidden_states:
179
+ assert torch.isfinite(hidden).all()
source/tests/test_triton_kernel.py CHANGED
@@ -4,21 +4,87 @@ import pytest
4
 
5
  torch = pytest.importorskip("torch")
6
 
 
7
 
8
- @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
9
- def test_packed_swiglu_forward_backward_parity() -> None:
10
- pytest.importorskip("triton")
 
 
 
 
11
  from qyrou_arch.triton_kernels import PackedSwiGLUFunction
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  torch.manual_seed(11)
14
  packed = torch.randn(7, 2 * 1328, device="cuda", dtype=torch.bfloat16, requires_grad=True)
15
  reference = packed.detach().clone().requires_grad_(True)
 
 
16
  output = PackedSwiGLUFunction.apply(packed)
17
- gate, up = reference.chunk(2, dim=-1)
18
- expected = torch.nn.functional.silu(gate) * up
19
  gradient = torch.randn_like(output)
20
  output.backward(gradient)
21
  expected.backward(gradient)
22
  torch.testing.assert_close(output, expected, atol=2e-2, rtol=2e-2)
23
  torch.testing.assert_close(packed.grad, reference.grad, atol=3e-2, rtol=3e-2)
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  torch = pytest.importorskip("torch")
6
 
7
+ cuda_available = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required")
8
 
9
+
10
+ def reference_swiglu(packed: torch.Tensor) -> torch.Tensor:
11
+ gate, up = packed.chunk(2, dim=-1)
12
+ return torch.nn.functional.silu(gate) * up
13
+
14
+
15
+ def assert_kernel_parity(packed: torch.Tensor, atol: float, rtol: float) -> None:
16
  from qyrou_arch.triton_kernels import PackedSwiGLUFunction
17
 
18
+ candidate = packed.detach().clone().requires_grad_(True)
19
+ output = PackedSwiGLUFunction.apply(candidate)
20
+ expected = reference_swiglu(packed.detach().requires_grad_(True))
21
+ gradient = torch.randn_like(output)
22
+ output.backward(gradient)
23
+ expected.backward(gradient)
24
+ torch.testing.assert_close(output, expected, atol=atol, rtol=rtol)
25
+ torch.testing.assert_close(candidate.grad, packed.grad, atol=atol * 2, rtol=rtol * 2)
26
+
27
+
28
+ @cuda_available
29
+ def test_packed_swiglu_forward_backward_parity() -> None:
30
+ pytest.importorskip("triton")
31
  torch.manual_seed(11)
32
  packed = torch.randn(7, 2 * 1328, device="cuda", dtype=torch.bfloat16, requires_grad=True)
33
  reference = packed.detach().clone().requires_grad_(True)
34
+ from qyrou_arch.triton_kernels import PackedSwiGLUFunction
35
+
36
  output = PackedSwiGLUFunction.apply(packed)
37
+ expected = reference_swiglu(reference)
 
38
  gradient = torch.randn_like(output)
39
  output.backward(gradient)
40
  expected.backward(gradient)
41
  torch.testing.assert_close(output, expected, atol=2e-2, rtol=2e-2)
42
  torch.testing.assert_close(packed.grad, reference.grad, atol=3e-2, rtol=3e-2)
43
 
44
+
45
+ @cuda_available
46
+ def test_packed_swiglu_fp32_parity() -> None:
47
+ pytest.importorskip("triton")
48
+ torch.manual_seed(21)
49
+ packed = torch.randn(11, 2 * 1328, device="cuda", dtype=torch.float32, requires_grad=True)
50
+ assert_kernel_parity(packed, atol=1e-4, rtol=1e-4)
51
+
52
+
53
+ @cuda_available
54
+ @pytest.mark.parametrize("n_cols", [3, 129, 1329])
55
+ def test_packed_swiglu_odd_tail_width(n_cols: int) -> None:
56
+ pytest.importorskip("triton")
57
+ torch.manual_seed(31)
58
+ packed = torch.randn(5, 2 * n_cols, device="cuda", dtype=torch.bfloat16, requires_grad=True)
59
+ assert_kernel_parity(packed, atol=2e-2, rtol=2e-2)
60
+
61
+
62
+ @cuda_available
63
+ def test_packed_swiglu_non_contiguous_input() -> None:
64
+ pytest.importorskip("triton")
65
+ torch.manual_seed(41)
66
+ base = torch.randn(7, 2 * 1328 + 4, device="cuda", dtype=torch.bfloat16, requires_grad=True)
67
+ packed = base[:, 2:-2]
68
+ assert packed.shape[-1] == 2 * 1328
69
+ assert not packed.is_contiguous()
70
+ assert_kernel_parity(packed, atol=2e-2, rtol=2e-2)
71
+
72
+
73
+ @cuda_available
74
+ def test_packed_swiglu_zero_size_rows() -> None:
75
+ pytest.importorskip("triton")
76
+ from qyrou_arch.triton_kernels import PackedSwiGLUFunction
77
+
78
+ packed = torch.empty(0, 2 * 1328, device="cuda", dtype=torch.bfloat16, requires_grad=True)
79
+ output = PackedSwiGLUFunction.apply(packed)
80
+ assert output.shape == (0, 1328)
81
+ output.sum().backward()
82
+ assert packed.grad is not None and packed.grad.shape == packed.shape
83
+
84
+
85
+ @cuda_available
86
+ def test_packed_swiglu_large_row_count() -> None:
87
+ pytest.importorskip("triton")
88
+ torch.manual_seed(51)
89
+ packed = torch.randn(4096, 2 * 1328, device="cuda", dtype=torch.bfloat16, requires_grad=True)
90
+ assert_kernel_parity(packed, atol=2e-2, rtol=2e-2)