Release Qyrou-1-EXP-Base: 65M hybrid GQA/conv model (serving-complete)
Browse files- README.md +34 -17
- modeling_qyrou_arch.py +116 -50
- source/.pytest_cache/README.md +8 -0
- source/QYROU_ARCH_ARCHITECTURE_AUDIT.md +67 -34
- source/qyrou_arch/modeling_qyrou_arch.py +116 -50
- source/scripts/benchmark_micro.py +152 -0
- source/tests/test_model.py +76 -0
- source/tests/test_reference.py +179 -0
- source/tests/test_triton_kernel.py +71 -5
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,
|
| 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
|
| 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 |
-
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 191 |
-
|
|
|
|
| 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 |
-
|
| 209 |
-
|
| 210 |
-
|
| 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 |
-
|
| 284 |
-
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
**
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 55 |
| 4 convolution blocks | 9,226,240 |
|
| 56 |
| Final RMSNorm | 512 |
|
| 57 |
-
| **Total** | **65,
|
| 58 |
|
| 59 |
-
An attention block has 2,696,
|
| 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 |
-
|
| 118 |
-
-
|
|
|
|
|
|
|
| 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 |
-
|
| 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.
|
| 144 |
-
|
| 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.
|
| 151 |
-
|
|
|
|
|
|
|
| 152 |
|
| 153 |
-
### 4. Serving outputs do not follow the full Transformers contract
|
| 154 |
|
| 155 |
-
|
| 156 |
-
the standard causal-LM generation inputs.
|
| 157 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
5. Add optional optimized paths behind capability checks, preserving a plain
|
| 203 |
-
PyTorch fallback
|
| 204 |
-
|
| 205 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 191 |
-
|
|
|
|
| 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 |
-
|
| 209 |
-
|
| 210 |
-
|
| 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 |
-
|
| 284 |
-
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
**
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 9 |
-
def
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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)
|