94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
1656 lines
66 KiB
Python
1656 lines
66 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
||
# Copyright 2023-2024 SGLang Team
|
||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
# you may not use this file except in compliance with the License.
|
||
# You may obtain a copy of the License at
|
||
#
|
||
# http://www.apache.org/licenses/LICENSE-2.0
|
||
#
|
||
# Unless required by applicable law or agreed to in writing, software
|
||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
# See the License for the specific language governing permissions and
|
||
# limitations under the License.
|
||
# ==============================================================================
|
||
"""Inference-only Zyphra ZAYA1 (CCA attention + MoE) model implementation.
|
||
|
||
Architecture summary (see docs/supported_models/text_generation/zaya_design.md
|
||
for the full design notes):
|
||
|
||
- Even-indexed layers run :class:`ZayaAttention`, which feeds hidden states to
|
||
the :class:`CCA` (Compressed Convolutional Attention) projection. CCA emits
|
||
q/k/v via two small (``kernel_size=2``) depthwise + grouped 1D convolutions
|
||
over the time axis plus a learnable per-K-head temperature. The conv needs a
|
||
two-token left padding that is sourced from a per-request state cache owned
|
||
by the CCA module itself. The q/k/v then go through partial rotary embedding
|
||
(``partial_rotary_factor=0.5``) and SGLang's :class:`RadixAttention` for the
|
||
softmax MHA. The implementation only uses ``torch`` / ``torch.nn`` ops, so the
|
||
same code runs on NVIDIA and AMD GPUs.
|
||
- Odd-indexed layers run :class:`ZayaBlock`, an MoE mixer built around SGLang's
|
||
:class:`FusedMoE`. Expert routing uses a 3-layer MLP with EDA (depth-wise
|
||
averaging across MoE layers) and MOD (mixture-of-depths skip expert).
|
||
- Per-layer :class:`ResidualScaling` keeps the residual stream in fp32 with
|
||
affine scale/bias both on the residual and on the post-mixer hidden states.
|
||
- Per-request CCA state (``conv_state`` + ``prev_hs``) lives in SGLang's
|
||
centralized ``MambaPool`` inside ``HybridReqToTokenPool``. The per-request
|
||
state plumbing (slot indices, prefix mask, cuda-graph buffers) is owned by
|
||
``ShortConvAttnBackend`` and reached via
|
||
``get_attn_backend().conv_state_metadata()``, so the model holds no pool
|
||
access; CCA runs its own conv (:func:`cca_extend` / :func:`cca_decode`)
|
||
against the returned handle.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import re
|
||
from collections.abc import Iterable
|
||
from typing import List, Optional, Tuple
|
||
|
||
import torch
|
||
import torch.nn.functional as F
|
||
from torch import nn
|
||
|
||
from sglang.srt.configs.zaya import ZayaConfig
|
||
from sglang.srt.distributed import (
|
||
get_pp_group,
|
||
tensor_model_parallel_all_reduce,
|
||
)
|
||
from sglang.srt.layers.layernorm import RMSNorm
|
||
from sglang.srt.layers.linear import (
|
||
ColumnParallelLinear,
|
||
ReplicatedLinear,
|
||
RowParallelLinear,
|
||
)
|
||
from sglang.srt.layers.logits_processor import LogitsProcessor
|
||
from sglang.srt.layers.moe.ep_moe.layer import get_moe_impl_class
|
||
from sglang.srt.layers.moe.topk import StandardTopKOutput
|
||
from sglang.srt.layers.quantization.base_config import QuantizationConfig
|
||
from sglang.srt.layers.radix_attention import RadixAttention
|
||
from sglang.srt.layers.rotary_embedding import get_rope
|
||
from sglang.srt.layers.utils import PPMissingLayer
|
||
from sglang.srt.layers.vocab_parallel_embedding import (
|
||
ParallelLMHead,
|
||
VocabParallelEmbedding,
|
||
)
|
||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
|
||
from sglang.srt.model_executor.forward_context import get_attn_backend
|
||
from sglang.srt.model_loader.weight_utils import default_weight_loader
|
||
from sglang.srt.runtime_context import get_parallel
|
||
from sglang.srt.utils import add_prefix, make_layers, set_weight_attrs
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Residual scaling
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class ResidualScaling(nn.Module):
|
||
"""Affine fp32 scaling applied to the residual / hidden_states streams.
|
||
|
||
Layer 0 has no incoming residual stream, so its checkpoint omits
|
||
``residual_scale`` / ``residual_bias`` and ``has_residual`` stays False.
|
||
"""
|
||
|
||
def __init__(self, config: ZayaConfig, layer_n: int) -> None:
|
||
super().__init__()
|
||
self.hidden_size = config.hidden_size
|
||
self.has_residual = layer_n != 0
|
||
self.hidden_states_scale = nn.Parameter(torch.ones(self.hidden_size))
|
||
self.hidden_states_bias = nn.Parameter(torch.zeros(self.hidden_size))
|
||
if self.has_residual:
|
||
self.residual_scale = nn.Parameter(torch.ones(self.hidden_size))
|
||
self.residual_bias = nn.Parameter(torch.zeros(self.hidden_size))
|
||
|
||
def forward(
|
||
self,
|
||
residual: Optional[torch.Tensor],
|
||
hidden_states: torch.Tensor,
|
||
) -> tuple[Optional[torch.Tensor], torch.Tensor]:
|
||
hs_scale = self.hidden_states_scale.to(torch.float32)
|
||
hs_bias = self.hidden_states_bias.to(torch.float32)
|
||
hidden_states = (hidden_states.float() + hs_bias) * hs_scale
|
||
|
||
if self.has_residual and residual is not None:
|
||
res_scale = self.residual_scale.to(torch.float32)
|
||
res_bias = self.residual_bias.to(torch.float32)
|
||
residual = (residual.float() + res_bias) * res_scale
|
||
|
||
return residual, hidden_states
|
||
|
||
|
||
def _apply_norm_with_fp32_residual(
|
||
norm: nn.Module,
|
||
residual: torch.Tensor,
|
||
target_dtype: torch.dtype,
|
||
) -> torch.Tensor:
|
||
"""Normalize ``residual`` (typically fp32) and cast back to ``target_dtype``.
|
||
|
||
The fp32 residual stream is preserved by the caller (the residual tensor
|
||
is kept around for the next accumulation), so the norm itself can run at
|
||
``target_dtype`` -- this lets us hit the fused sgl_kernel rmsnorm path
|
||
instead of the eager ``forward_native`` fallback (5+ kernel launches per
|
||
call, ×120 norms per step).
|
||
"""
|
||
return norm(residual.to(target_dtype))
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# CCA conv-state kernels (v1 torch)
|
||
#
|
||
# ZAYA1-specific conv step: the CCA conv is a causal two-stage conv over
|
||
# ``qk = [W_q hs || W_k hs]`` plus a one-token ``prev_hs`` lag for val_proj2.
|
||
# The per-request conv state lives in the centralized MambaPool; the backend
|
||
# (ShortConvAttnBackend) hands out the slot indices + prefix flags and CCA runs
|
||
# these functions against them. ``conv_qk`` is the module's two-stage conv;
|
||
# both functions mutate ``conv_state`` / ``prev_hs_state`` in place and return
|
||
# ``(qk_out, v2_input)`` -- the conv output ``[T, in_out_ch]`` and the (shifted)
|
||
# ``val_proj2`` input ``[T, hidden_size]``.
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def cca_extend(
|
||
qk: torch.Tensor,
|
||
hidden_states: torch.Tensor,
|
||
conv_qk: nn.Module,
|
||
conv_state: torch.Tensor,
|
||
prev_hs_state: torch.Tensor,
|
||
slot_ids: List[int],
|
||
has_prefix: List[bool],
|
||
extend_seq_lens_cpu: List[int],
|
||
total_padding: Optional[int] = None,
|
||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||
"""Prefill / extend conv-state step (v1, pure torch).
|
||
|
||
Walks each request in the batch, applies ``conv_qk`` with the request's own
|
||
initial state (zeros on a fresh first chunk, the cached ``conv_state`` slot
|
||
otherwise), writes the updated ``conv_state`` / ``prev_hs_state`` back, and
|
||
returns the concatenated ``(qk_out, v2_input)`` in the original token layout.
|
||
|
||
``slot_ids`` is the host mirror of the per-request MambaPool slot indices and
|
||
``has_prefix[i]`` is ``True`` when request ``i`` resumes a cached prefix.
|
||
|
||
The Triton swap (:func:`cca_conv1d_fn`) removes this per-request loop.
|
||
"""
|
||
dtype = hidden_states.dtype
|
||
if total_padding is None:
|
||
total_padding = conv_state.shape[-1]
|
||
in_out_ch = qk.shape[-1]
|
||
hidden_size = hidden_states.shape[-1]
|
||
|
||
qk_out = torch.empty_like(qk)
|
||
v2_input = torch.empty_like(hidden_states)
|
||
|
||
# Fresh-prefill fast path: when no request has a cached prefix the per-request
|
||
# convs can be coalesced into a single packed convolution. Each request's
|
||
# segment is laid out as ``[total_padding zeros, S_i tokens]``.
|
||
all_fresh = bool(extend_seq_lens_cpu) and not any(has_prefix)
|
||
|
||
if all_fresh:
|
||
seq_lens = [int(s) for s in extend_seq_lens_cpu]
|
||
pad = total_padding
|
||
offsets_in = [0]
|
||
for s in seq_lens:
|
||
offsets_in.append(offsets_in[-1] + s + pad)
|
||
packed = qk.new_zeros((1, in_out_ch, offsets_in[-1]))
|
||
start = 0
|
||
for i, s in enumerate(seq_lens):
|
||
end = start + s
|
||
packed[0, :, offsets_in[i] + pad : offsets_in[i + 1]] = qk[
|
||
start:end
|
||
].transpose(0, 1)
|
||
start = end
|
||
|
||
packed_out = conv_qk(packed) # [1, C, offsets_in[-1] - pad]
|
||
|
||
start = 0
|
||
for i, s in enumerate(seq_lens):
|
||
end = start + s
|
||
a_i = offsets_in[i]
|
||
qk_out[start:end] = packed_out[0, :, a_i : a_i + s].transpose(0, 1)
|
||
new_state = packed[0, :, a_i + s : a_i + s + pad]
|
||
conv_state[slot_ids[i]] = new_state.to(conv_state.dtype)
|
||
|
||
hs_cur = hidden_states[start:end]
|
||
first = hidden_states.new_zeros((1, hidden_size))
|
||
v2_input[start:end] = torch.cat([first, hs_cur[:-1]], dim=0)
|
||
prev_hs_state[slot_ids[i]] = (
|
||
hs_cur[-1].unsqueeze(-1).to(prev_hs_state.dtype)
|
||
)
|
||
start = end
|
||
else:
|
||
start = 0
|
||
for i, seq_len in enumerate(extend_seq_lens_cpu):
|
||
end = start + int(seq_len)
|
||
slot = slot_ids[i]
|
||
prefix = bool(has_prefix[i])
|
||
|
||
qk_cur = qk[start:end].transpose(0, 1).unsqueeze(0) # [1, C, S_cur]
|
||
if prefix:
|
||
left_pad = conv_state[slot].unsqueeze(0).to(dtype)
|
||
else:
|
||
left_pad = qk_cur.new_zeros((1, in_out_ch, total_padding))
|
||
padded = torch.cat([left_pad, qk_cur], dim=-1)
|
||
|
||
out = conv_qk(padded) # [1, C, S_cur]
|
||
qk_out[start:end] = out.squeeze(0).transpose(0, 1)
|
||
|
||
new_state = padded[..., -total_padding:]
|
||
conv_state[slot] = new_state.squeeze(0).to(conv_state.dtype)
|
||
|
||
hs_cur = hidden_states[start:end]
|
||
if prefix:
|
||
first = prev_hs_state[slot].squeeze(-1).to(dtype).unsqueeze(0)
|
||
else:
|
||
first = hidden_states.new_zeros((1, hidden_size))
|
||
v2_input[start:end] = torch.cat([first, hs_cur[:-1]], dim=0)
|
||
|
||
prev_hs_state[slot] = hs_cur[-1].unsqueeze(-1).to(prev_hs_state.dtype)
|
||
start = end
|
||
|
||
return qk_out, v2_input
|
||
|
||
|
||
def cca_decode(
|
||
qk: torch.Tensor,
|
||
hidden_states: torch.Tensor,
|
||
conv_qk: nn.Module,
|
||
conv_state: torch.Tensor,
|
||
prev_hs_state: torch.Tensor,
|
||
mamba_indices: torch.Tensor,
|
||
total_padding: Optional[int] = None,
|
||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||
"""Single-token decode conv-state step (v1, pure torch).
|
||
|
||
Gathers each request's cached ``conv_state`` / ``prev_hs_state`` via
|
||
``index_select``, runs ``conv_qk`` on the ``[T, C, total_padding + 1]``
|
||
window, and scatters the updated state back with ``index_copy_``. All ops are
|
||
on-device (``mamba_indices`` is a device ``long`` tensor), so this stays
|
||
CUDA-graph capturable. Returns ``(qk_out, prev_hs)`` where ``prev_hs`` is the
|
||
previous hidden state feeding ``val_proj2``.
|
||
|
||
The Triton swap is :func:`cca_conv1d_update`.
|
||
"""
|
||
dtype = hidden_states.dtype
|
||
if total_padding is None:
|
||
total_padding = conv_state.shape[-1]
|
||
|
||
left_pad = conv_state.index_select(0, mamba_indices).to(dtype)
|
||
cur = qk.unsqueeze(-1) # [T, C, 1]
|
||
padded = torch.cat([left_pad, cur], dim=-1) # [T, C, total_padding + 1]
|
||
out = conv_qk(padded) # [T, C, 1]
|
||
qk_out = out.squeeze(-1) # [T, C]
|
||
|
||
new_state = padded[..., -total_padding:]
|
||
conv_state.index_copy_(0, mamba_indices, new_state.to(conv_state.dtype))
|
||
|
||
# Read the previous hidden state (val_proj2 input) BEFORE overwriting the
|
||
# slot with the current token.
|
||
prev_hs = prev_hs_state.index_select(0, mamba_indices).squeeze(-1).to(dtype)
|
||
prev_hs_state.index_copy_(
|
||
0, mamba_indices, hidden_states.unsqueeze(-1).to(prev_hs_state.dtype)
|
||
)
|
||
return qk_out, prev_hs
|
||
|
||
|
||
# Fused kernel seam (TODO) -- perf swap for the v1 torch paths above. These
|
||
# mirror the ``causal_conv1d_fn`` / ``causal_conv1d_update`` contract but for
|
||
# CCA's two-stage *grouped* conv (conv_qk[0] depthwise + conv_qk[1] grouped
|
||
# per-head), which the stock depthwise ``causal_conv1d`` cannot express. Once
|
||
# implemented they replace the per-request loop in ``cca_extend`` and the
|
||
# separate gather/conv/scatter launches in ``cca_decode`` with a single
|
||
# index-driven kernel. Same ``(qk_out, v2_input)`` return contract.
|
||
|
||
|
||
def cca_conv1d_fn(*args, **kwargs):
|
||
raise NotImplementedError(
|
||
"Fused CCA prefill conv-with-state kernel not implemented yet; "
|
||
"the model uses cca_extend (v1 torch) in the meantime."
|
||
)
|
||
|
||
|
||
def cca_conv1d_update(*args, **kwargs):
|
||
raise NotImplementedError(
|
||
"Fused CCA decode conv-with-state kernel not implemented yet; "
|
||
"the model uses cca_decode (v1 torch) in the meantime."
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# CCA: Compressed Convolutional Attention QKV projection
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class CCA(nn.Module):
|
||
"""Compressed Convolutional Attention QKV projection.
|
||
|
||
Given hidden states ``hs`` of shape ``[S, H]`` this layer produces
|
||
``(q, k, v)`` where:
|
||
|
||
q = (W_q hs + Conv(W_q hs ‖ W_k hs)_q) / 2
|
||
+ mean_group(W_k hs) / 2 (fp32, RMSNorm'd)
|
||
k = (W_k hs + Conv(W_q hs ‖ W_k hs)_k) / 2
|
||
+ mean_group(W_q hs) / 2, scaled by per-head temperature
|
||
v = concat(W_{v1} hs, W_{v2} hs_prev_shifted)
|
||
|
||
The two-stage conv on ``(W_q hs ‖ W_k hs)`` needs
|
||
``total_padding = (cca_time0 - 1) + (cca_time1 - 1)`` tokens of left padding.
|
||
For the first prefill chunk of a request the padding is zero; for a resumed
|
||
prefill or for decode it is read from a per-request cache that this module
|
||
maintains internally.
|
||
|
||
Parallelism: when ``tp_size > 1`` the CCA is head-parallel. Both the
|
||
grouped-mean step and the second ``conv_qk`` stage with
|
||
``groups=num_q_heads+num_k_heads`` are head-local (each GQA group lives on
|
||
a single rank), so the entire QKV projection runs without any cross-rank
|
||
collective. The QKV projections become ``ColumnParallelLinear`` and the
|
||
two ``nn.Conv1d`` layers are sized per-rank with custom weight loaders
|
||
that slice the HF checkpoint rows into ``[rank's q heads, rank's k heads]``.
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
config: ZayaConfig,
|
||
cca_num_k_heads: int,
|
||
cca_num_q_heads: int,
|
||
hidden_size: int,
|
||
head_dim: int,
|
||
cca_time0: int,
|
||
cca_time1: int,
|
||
layer_id: int,
|
||
quant_config: Optional[QuantizationConfig] = None,
|
||
prefix: str = "",
|
||
tp_rank: Optional[int] = None,
|
||
tp_size: Optional[int] = None,
|
||
) -> None:
|
||
super().__init__()
|
||
self.config = config
|
||
self.layer_id = layer_id
|
||
self.hidden_size = int(hidden_size)
|
||
self.head_dim = int(head_dim)
|
||
self.cca_time0 = int(cca_time0)
|
||
self.cca_time1 = int(cca_time1)
|
||
self.padding0 = self.cca_time0 - 1
|
||
self.padding1 = self.cca_time1 - 1
|
||
self.total_padding = self.padding0 + self.padding1
|
||
|
||
if tp_rank is None:
|
||
tp_rank = get_parallel().tp_rank
|
||
if tp_size is None:
|
||
tp_size = get_parallel().tp_size
|
||
self.tp_rank = int(tp_rank)
|
||
self.tp_size = int(tp_size)
|
||
|
||
# Full (global) head counts retained for weight loading and shape asserts.
|
||
self.num_q_heads_full = int(cca_num_q_heads)
|
||
self.num_k_heads_full = int(cca_num_k_heads)
|
||
assert (
|
||
self.num_q_heads_full % self.num_k_heads_full == 0
|
||
), "num_q_heads must be a multiple of num_k_heads"
|
||
self.gqa_groups = self.num_q_heads_full // self.num_k_heads_full
|
||
|
||
# Head-parallel TP requires both head counts to be divisible by tp_size.
|
||
# KV-replication-style TP (tp_size > num_k_heads) is not yet supported.
|
||
assert self.num_q_heads_full % self.tp_size == 0, (
|
||
f"num_q_heads ({self.num_q_heads_full}) must be divisible by "
|
||
f"tp_size ({self.tp_size}) for ZAYA1 head-parallel CCA"
|
||
)
|
||
assert self.num_k_heads_full % self.tp_size == 0, (
|
||
f"num_k_heads ({self.num_k_heads_full}) must be divisible by "
|
||
f"tp_size ({self.tp_size}); KV-replication TP is not supported "
|
||
"for ZAYA1 because both grouped-mean and conv_qk.1 are per-head"
|
||
)
|
||
|
||
# Per-rank head counts.
|
||
self.num_q_heads = self.num_q_heads_full // self.tp_size
|
||
self.num_k_heads = self.num_k_heads_full // self.tp_size
|
||
|
||
# Per-rank channel layout.
|
||
self.latent_q_dim_full = self.num_q_heads_full * self.head_dim
|
||
self.latent_k_dim_full = self.num_k_heads_full * self.head_dim
|
||
self.in_out_ch_full = self.latent_q_dim_full + self.latent_k_dim_full
|
||
self.latent_q_dim = self.num_q_heads * self.head_dim
|
||
self.latent_k_dim = self.num_k_heads * self.head_dim
|
||
self.in_out_ch = self.latent_q_dim + self.latent_k_dim
|
||
self.sqrt_head_dim = float(self.head_dim) ** 0.5
|
||
self.clamp_temp = bool(getattr(config, "clamp_temp", False))
|
||
|
||
bias = bool(getattr(config, "attention_bias", False))
|
||
# ``linear_q`` / ``linear_k`` outputs are laid out as a contiguous head
|
||
# sequence in the HF checkpoint, so the natural ColumnParallel shard
|
||
# (``tp_rank * shard``) lands rank ``r`` on the head set
|
||
# ``[r * heads_per_rank, (r+1) * heads_per_rank)``.
|
||
#
|
||
# At ``tp_size == 1`` there is nothing to shard, and on ROCm/aiter the
|
||
# ColumnParallelLinear path selects a slower GEMM for the large-M prefill
|
||
# (1.6-2.25x slower than ReplicatedLinear in bench_one_batch), so the
|
||
# single-GPU case uses ReplicatedLinear. ``tp_size > 1`` keeps
|
||
# ColumnParallelLinear for the per-rank head shard.
|
||
if self.tp_size > 1:
|
||
self.linear_q = ColumnParallelLinear(
|
||
self.hidden_size,
|
||
self.latent_q_dim_full,
|
||
bias=bias,
|
||
gather_output=False,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("linear_q", prefix),
|
||
tp_rank=self.tp_rank,
|
||
tp_size=self.tp_size,
|
||
)
|
||
self.linear_k = ColumnParallelLinear(
|
||
self.hidden_size,
|
||
self.latent_k_dim_full,
|
||
bias=bias,
|
||
gather_output=False,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("linear_k", prefix),
|
||
tp_rank=self.tp_rank,
|
||
tp_size=self.tp_size,
|
||
)
|
||
else:
|
||
self.linear_q = ReplicatedLinear(
|
||
self.hidden_size,
|
||
self.latent_q_dim_full,
|
||
bias=bias,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("linear_q", prefix),
|
||
)
|
||
self.linear_k = ReplicatedLinear(
|
||
self.hidden_size,
|
||
self.latent_k_dim_full,
|
||
bias=bias,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("linear_k", prefix),
|
||
)
|
||
# The HF V-projection layout maps val_proj1 to the FIRST half of K
|
||
# heads and val_proj2 to the SECOND half (after ``cat([v1, v2]).view(
|
||
# T, num_k_heads_full, head_dim)``). That doesn't align with a simple
|
||
# output-dim ColumnParallel shard, so val_proj1 / val_proj2 are kept
|
||
# Replicated and the per-rank K-head slice is taken in the forward
|
||
# passes after ``cat + view``. The replicated weight memory is small
|
||
# (~0.5 MB / layer) and the wasted compute is negligible compared to
|
||
# linear_q / linear_k / o_proj.
|
||
self.val_proj1 = ReplicatedLinear(
|
||
self.hidden_size,
|
||
self.latent_k_dim_full // 2,
|
||
bias=bias,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("val_proj1", prefix),
|
||
)
|
||
self.val_proj2 = ReplicatedLinear(
|
||
self.hidden_size,
|
||
self.latent_k_dim_full // 2,
|
||
bias=bias,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("val_proj2", prefix),
|
||
)
|
||
|
||
# Per-rank K head range, used for slicing the replicated v tensor.
|
||
self.k_head_start = self.tp_rank * self.num_k_heads
|
||
self.k_head_end = self.k_head_start + self.num_k_heads
|
||
|
||
# Two-stage depthwise + grouped conv along the time axis, sized for
|
||
# this rank's head subset. Wrapping the two nn.Conv1d modules in
|
||
# nn.Sequential makes the HF checkpoint keys ``conv_qk.{0,1}.weight``
|
||
# / ``conv_qk.{0,1}.bias`` map onto submodules 1:1, with TP slicing
|
||
# handled by the custom weight_loader attached below.
|
||
self.conv_qk = nn.Sequential(
|
||
nn.Conv1d(
|
||
in_channels=self.in_out_ch,
|
||
out_channels=self.in_out_ch,
|
||
kernel_size=self.cca_time0,
|
||
groups=self.in_out_ch,
|
||
padding=0,
|
||
stride=1,
|
||
),
|
||
nn.Conv1d(
|
||
in_channels=self.in_out_ch,
|
||
out_channels=self.in_out_ch,
|
||
kernel_size=self.cca_time1,
|
||
groups=(self.num_k_heads + self.num_q_heads),
|
||
padding=0,
|
||
stride=1,
|
||
),
|
||
)
|
||
|
||
# Per-K-head learnable temperature scalar (per-rank slice).
|
||
self.temp = nn.Parameter(torch.zeros(self.num_k_heads))
|
||
|
||
# Attach TP-aware weight loaders to conv_qk weights/biases and ``temp``
|
||
# so the existing ``load_weights`` dispatch (``getattr(param,
|
||
# "weight_loader", default_weight_loader)``) automatically slices the
|
||
# HF checkpoint into rank-local rows.
|
||
if self.tp_size > 1:
|
||
self._install_tp_weight_loaders()
|
||
|
||
# ----- TP weight loaders ----------------------------------------------
|
||
|
||
def _install_tp_weight_loaders(self) -> None:
|
||
"""Attach TP-aware ``weight_loader`` attributes to parameters whose
|
||
full-tensor → per-rank slicing cannot be expressed by a generic
|
||
ColumnParallelLinear loader: the two ``conv_qk`` Conv1d weights and
|
||
biases (where the per-rank "row" set is the discontiguous union of
|
||
this rank's q heads and this rank's k heads) and the per-K-head
|
||
``temp`` parameter.
|
||
"""
|
||
head_dim = self.head_dim
|
||
latent_q_dim_full = self.latent_q_dim_full
|
||
num_q_heads_per_rank = self.num_q_heads
|
||
num_k_heads_per_rank = self.num_k_heads
|
||
tp_rank = self.tp_rank
|
||
|
||
q_start = tp_rank * num_q_heads_per_rank * head_dim
|
||
q_end = q_start + num_q_heads_per_rank * head_dim
|
||
k_start = latent_q_dim_full + tp_rank * num_k_heads_per_rank * head_dim
|
||
k_end = k_start + num_k_heads_per_rank * head_dim
|
||
k_temp_start = tp_rank * num_k_heads_per_rank
|
||
k_temp_end = k_temp_start + num_k_heads_per_rank
|
||
|
||
def conv_row_loader(param: torch.Tensor, loaded_weight: torch.Tensor) -> None:
|
||
# Both Conv1d.weight ([C_out, in_per_group, K]) and Conv1d.bias
|
||
# ([C_out]) slice along the leading (output channel) dim. The
|
||
# per-rank rows are the rank's q heads (contiguous) followed by
|
||
# the rank's k heads (contiguous in the second half of the full
|
||
# tensor).
|
||
sliced = torch.cat(
|
||
[loaded_weight[q_start:q_end], loaded_weight[k_start:k_end]],
|
||
dim=0,
|
||
)
|
||
assert (
|
||
sliced.shape == param.data.shape
|
||
), f"conv shard shape mismatch: {sliced.shape} vs {param.data.shape}"
|
||
param.data.copy_(sliced)
|
||
|
||
def temp_loader(param: torch.Tensor, loaded_weight: torch.Tensor) -> None:
|
||
sliced = loaded_weight[k_temp_start:k_temp_end]
|
||
assert (
|
||
sliced.shape == param.data.shape
|
||
), f"temp shard shape mismatch: {sliced.shape} vs {param.data.shape}"
|
||
param.data.copy_(sliced)
|
||
|
||
set_weight_attrs(self.conv_qk[0].weight, {"weight_loader": conv_row_loader})
|
||
set_weight_attrs(self.conv_qk[0].bias, {"weight_loader": conv_row_loader})
|
||
set_weight_attrs(self.conv_qk[1].weight, {"weight_loader": conv_row_loader})
|
||
set_weight_attrs(self.conv_qk[1].bias, {"weight_loader": conv_row_loader})
|
||
set_weight_attrs(self.temp, {"weight_loader": temp_loader})
|
||
|
||
# ----- helpers ---------------------------------------------------------
|
||
|
||
def _normalize_qk(
|
||
self, query: torch.Tensor, key: torch.Tensor
|
||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||
"""RMSNorm (no learnable weight) + sqrt(head_dim) scaling on q and k,
|
||
plus per-K-head temperature on k. Computed in fp32 for stability.
|
||
"""
|
||
eps = 1e-12
|
||
sqrt_head_dim = float(self.sqrt_head_dim)
|
||
query_fp32 = query.to(torch.float32)
|
||
inv_q = (
|
||
torch.rsqrt(query_fp32.pow(2).sum(-1, keepdim=True) + eps) * sqrt_head_dim
|
||
)
|
||
query_fp32 = query_fp32 * inv_q
|
||
|
||
key_fp32 = key.to(torch.float32)
|
||
inv_k = torch.rsqrt(key_fp32.pow(2).sum(-1, keepdim=True) + eps) * sqrt_head_dim
|
||
key_fp32 = key_fp32 * inv_k
|
||
temp = self.temp.to(torch.float32).view(1, self.num_k_heads, 1)
|
||
if self.clamp_temp:
|
||
temp = torch.exp(torch.clamp(temp, 1e-7, 2.0))
|
||
key_fp32 = key_fp32 * temp
|
||
return query_fp32, key_fp32
|
||
|
||
def _add_grouped_qk_means(
|
||
self,
|
||
query_conv: torch.Tensor,
|
||
key_conv: torch.Tensor,
|
||
query_pre: torch.Tensor,
|
||
key_base: torch.Tensor,
|
||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||
"""Blend the post-conv q/k with the per-GQA-group mean of the
|
||
pre-conv (raw projection) q/k, matching the ZAYA1 training formula.
|
||
|
||
Shapes (T = num_tokens):
|
||
query_conv : [T, num_q_heads, head_dim] (fp32, post conv)
|
||
key_conv : [T, num_k_heads, head_dim] (fp32, post conv)
|
||
query_pre : [T, num_q_heads, head_dim] (raw W_q hs)
|
||
key_base : [T, num_k_heads, head_dim] (raw W_k hs)
|
||
"""
|
||
num_k_heads = key_base.shape[-2]
|
||
key_base_fp32 = key_base.to(torch.float32)
|
||
query_pre_grouped = query_pre.view(
|
||
query_pre.shape[0], num_k_heads, self.gqa_groups, query_pre.shape[-1]
|
||
)
|
||
query_pre_grouped_fp32 = query_pre_grouped.to(torch.float32)
|
||
query_out_grouped = (
|
||
query_conv.view_as(query_pre_grouped).to(torch.float32)
|
||
+ 0.5 * query_pre_grouped_fp32
|
||
+ 0.5 * key_base_fp32.unsqueeze(-2)
|
||
)
|
||
query_out = query_out_grouped.reshape(
|
||
query_pre.shape[0], -1, query_pre.shape[-1]
|
||
)
|
||
|
||
query_pre_mean = query_pre_grouped_fp32.mean(dim=-2, dtype=torch.float32)
|
||
key_out = (
|
||
key_conv.to(torch.float32) + 0.5 * query_pre_mean + 0.5 * key_base_fp32
|
||
)
|
||
return query_out, key_out
|
||
|
||
def _conv_qk_run(self, padded: torch.Tensor) -> torch.Tensor:
|
||
"""Run ``conv_qk`` on ``[N, C, S + total_padding]`` → ``[N, C, S]``."""
|
||
return self.conv_qk(padded)
|
||
|
||
# ----- forward modes ---------------------------------------------------
|
||
|
||
def _slice_v_per_rank(self, value_full: torch.Tensor) -> torch.Tensor:
|
||
"""Take this rank's K-head slice of the full ``value`` tensor.
|
||
|
||
Returns a no-op view when ``tp_size == 1``. For ``tp_size > 1`` the
|
||
full V tensor is computed on every rank (see the comment on
|
||
``val_proj1`` / ``val_proj2``) and the rank's contiguous K-head range
|
||
is selected here, leaving the downstream RadixAttention call with a
|
||
per-rank shape ``[T, num_k_heads_per_rank, head_dim]``.
|
||
"""
|
||
if self.tp_size == 1:
|
||
return value_full
|
||
return value_full[:, self.k_head_start : self.k_head_end, :].contiguous()
|
||
|
||
def _forward_no_state(
|
||
self, hs: torch.Tensor
|
||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||
"""Reference path: process the entire ``hs`` of shape ``[S, H]`` with
|
||
a zero initial conv state and a zero ``prev_hs``.
|
||
|
||
Exercised by the CCA unit tests so the prefill / decode paths can be
|
||
compared against a single-shot torch reference, and used as a fallback
|
||
for profile / warmup runs where no state cache is meaningful.
|
||
"""
|
||
S = hs.shape[0]
|
||
hs_3d = hs.unsqueeze(1) # [S, 1, H]
|
||
|
||
q_raw, _ = self.linear_q(hs_3d) # [S, 1, latent_q_dim_per_rank]
|
||
k_raw, _ = self.linear_k(hs_3d) # [S, 1, latent_k_dim_per_rank]
|
||
qk = torch.cat([q_raw, k_raw], dim=-1) # [S, 1, in_out_ch_per_rank]
|
||
|
||
query_pre = q_raw.view(S, self.num_q_heads, self.head_dim)
|
||
key_base = k_raw.view(S, self.num_k_heads, self.head_dim)
|
||
|
||
# [1, C, S+pad] -> [1, C, S]
|
||
qk_perm = qk.permute(1, 2, 0)
|
||
qk_pad = F.pad(qk_perm, (self.total_padding, 0))
|
||
qk_out = self._conv_qk_run(qk_pad).permute(2, 0, 1).squeeze(1) # [S, C]
|
||
|
||
query_conv = qk_out[:, : self.latent_q_dim].view(
|
||
S, self.num_q_heads, self.head_dim
|
||
)
|
||
key_conv = qk_out[:, self.latent_q_dim :].view(
|
||
S, self.num_k_heads, self.head_dim
|
||
)
|
||
|
||
query, key = self._add_grouped_qk_means(
|
||
query_conv, key_conv, query_pre, key_base
|
||
)
|
||
query, key = self._normalize_qk(query, key)
|
||
|
||
# val_proj1 / val_proj2 are replicated; compute the full V tensor and
|
||
# then take this rank's K-head slice.
|
||
# val_proj2 uses a right-shifted hidden_state. First val_proj2 input is 0.
|
||
hs_shifted = F.pad(hs_3d[:-1], (0, 0, 0, 0, 1, 0)) # [S, 1, H]
|
||
v1, _ = self.val_proj1(hs_3d)
|
||
v2, _ = self.val_proj2(hs_shifted)
|
||
value_full = (
|
||
torch.cat([v1, v2], dim=-1)
|
||
.squeeze(1)
|
||
.view(S, self.num_k_heads_full, self.head_dim)
|
||
)
|
||
value = self._slice_v_per_rank(value_full)
|
||
return query, key, value
|
||
|
||
def forward(
|
||
self,
|
||
hidden_states: torch.Tensor,
|
||
forward_batch: ForwardBatch,
|
||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||
"""Project ``hidden_states`` into ``(q, k, v)`` honoring per-request state.
|
||
|
||
The per-request conv-state plumbing (slot gather/scatter, prefix mask,
|
||
cuda-graph buffers) is owned by :class:`ShortConvAttnBackend
|
||
<sglang.srt.layers.attention.linear.short_conv_backend.ShortConvAttnBackend>`,
|
||
reached via ``get_attn_backend().conv_state_metadata``; CCA runs its own
|
||
two-stage grouped conv (:func:`cca_extend` / :func:`cca_decode`) against
|
||
that handle, so this module holds no pool access. Those functions return
|
||
the conv output ``qk_out`` and the ``val_proj2`` input ``v2_input`` (the
|
||
shifted / previous hidden state), updating the ``conv_state`` /
|
||
``prev_hs`` pool slots in place.
|
||
|
||
``q`` / ``k`` are returned in fp32 (the normalize step keeps fp32 for
|
||
stability); ``v`` is returned in the input dtype since the caller
|
||
casts everything back to ``hidden_states.dtype`` before rotary +
|
||
attention anyway.
|
||
|
||
Shapes::
|
||
|
||
q : [T, num_q_heads, head_dim]
|
||
k : [T, num_k_heads, head_dim]
|
||
v : [T, num_k_heads, head_dim]
|
||
"""
|
||
if hidden_states.shape[0] == 0:
|
||
zero = hidden_states.new_zeros((0,))
|
||
return (
|
||
zero.view(0, self.num_q_heads, self.head_dim).to(torch.float32),
|
||
zero.view(0, self.num_k_heads, self.head_dim).to(torch.float32),
|
||
zero.view(0, self.num_k_heads, self.head_dim),
|
||
)
|
||
|
||
T = hidden_states.shape[0]
|
||
q_raw, _ = self.linear_q(hidden_states) # [T, latent_q]
|
||
k_raw, _ = self.linear_k(hidden_states)
|
||
qk = torch.cat([q_raw, k_raw], dim=-1) # [T, in_out_ch]
|
||
|
||
query_pre = q_raw.view(T, self.num_q_heads, self.head_dim)
|
||
key_base = k_raw.view(T, self.num_k_heads, self.head_dim)
|
||
|
||
# The backend hands out the per-request conv-state handle (slot indices,
|
||
# prefix mask, cuda-graph buffers); CCA runs its own two-stage grouped
|
||
# conv against it and gets back the conv output + val_proj2 input, with
|
||
# the conv_state / prev_hs pool slots updated in place.
|
||
meta = get_attn_backend().conv_state_metadata(self.layer_id, forward_batch)
|
||
conv_state = meta.layer_cache.conv[0]
|
||
prev_hs_state = meta.layer_cache.conv[1]
|
||
if forward_batch.forward_mode.is_decode_or_idle():
|
||
qk_out, v2_input = cca_decode(
|
||
qk,
|
||
hidden_states,
|
||
self.conv_qk,
|
||
conv_state,
|
||
prev_hs_state,
|
||
meta.cache_indices,
|
||
self.total_padding,
|
||
)
|
||
else:
|
||
qk_out, v2_input = cca_extend(
|
||
qk,
|
||
hidden_states,
|
||
self.conv_qk,
|
||
conv_state,
|
||
prev_hs_state,
|
||
meta.slot_ids_cpu,
|
||
meta.has_prefix_cpu,
|
||
forward_batch.extend_seq_lens_cpu,
|
||
self.total_padding,
|
||
)
|
||
|
||
query_conv = qk_out[:, : self.latent_q_dim].view(
|
||
T, self.num_q_heads, self.head_dim
|
||
)
|
||
key_conv = qk_out[:, self.latent_q_dim :].view(
|
||
T, self.num_k_heads, self.head_dim
|
||
)
|
||
|
||
query, key = self._add_grouped_qk_means(
|
||
query_conv, key_conv, query_pre, key_base
|
||
)
|
||
query, key = self._normalize_qk(query, key)
|
||
|
||
v1, _ = self.val_proj1(hidden_states)
|
||
v2, _ = self.val_proj2(v2_input)
|
||
value_full = torch.cat([v1, v2], dim=-1).view(
|
||
T, self.num_k_heads_full, self.head_dim
|
||
)
|
||
value = self._slice_v_per_rank(value_full)
|
||
return query, key, value
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Attention layer (CCA QKV + rotary + RadixAttention)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class ZayaAttention(nn.Module):
|
||
def __init__(
|
||
self,
|
||
config: ZayaConfig,
|
||
layer_id: int,
|
||
quant_config: Optional[QuantizationConfig] = None,
|
||
prefix: str = "",
|
||
) -> None:
|
||
super().__init__()
|
||
self.config = config
|
||
self.layer_id = layer_id
|
||
self.hidden_size = config.hidden_size
|
||
self.num_q_heads_full = config.num_attention_heads
|
||
self.num_k_heads_full = config.num_query_groups
|
||
self.head_dim = config.head_dim
|
||
|
||
# Head-parallel TP: split both Q and KV heads across ranks. Since the
|
||
# grouped-mean and conv_qk.1 are head-local, no cross-rank collective
|
||
# is required inside the QKV projection. Both head counts must be
|
||
# divisible by tp_size; the KV-replicated GQA-TP variant (tp_size >
|
||
# num_k_heads) is intentionally rejected with a clear error message
|
||
# because both per-K-head paths assume each rank holds whole K heads.
|
||
self.tp_rank = get_parallel().tp_rank
|
||
self.tp_size = get_parallel().tp_size
|
||
# The head split, the ``o_proj`` RowParallel all-reduce, and the
|
||
# RadixAttention KV cache are all organized on the *global* TP group,
|
||
# and ``ZayaConfig.mamba2_cache_params`` sizes the conv-state cache on
|
||
# that same group. DP attention would run attention on the smaller
|
||
# attention-TP group (and ``o_proj`` would need
|
||
# ``use_dp_attention_reduce``), which this model does not wire up, so
|
||
# require the two groups to coincide and fail fast instead of silently
|
||
# mis-sizing the conv-state cache.
|
||
attn_tp_size = get_parallel().attn_tp_size
|
||
assert attn_tp_size == self.tp_size, (
|
||
f"ZAYA1 head-parallel attention requires the attention TP group "
|
||
f"({attn_tp_size}) to equal the global TP group ({self.tp_size}); "
|
||
"DP attention (enable_dp_attention) is not supported for ZAYA1."
|
||
)
|
||
assert self.num_q_heads_full % self.tp_size == 0, (
|
||
f"num_attention_heads ({self.num_q_heads_full}) must be divisible "
|
||
f"by tp_size ({self.tp_size}) for ZAYA1 head-parallel attention"
|
||
)
|
||
assert self.num_k_heads_full % self.tp_size == 0, (
|
||
f"num_query_groups ({self.num_k_heads_full}) must be divisible by "
|
||
f"tp_size ({self.tp_size}); set tp_size <= num_k_heads to keep "
|
||
"both grouped-mean and conv_qk.1 head-local on each rank"
|
||
)
|
||
self.num_q_heads = self.num_q_heads_full // self.tp_size
|
||
self.num_k_heads = self.num_k_heads_full // self.tp_size
|
||
self.q_dim_full = self.num_q_heads_full * self.head_dim
|
||
self.scale = self.head_dim**-0.5
|
||
|
||
# The HF checkpoint stores the CCA QKV projection under
|
||
# ``self_attn.qkv.*``, so the CCA submodule is registered with that
|
||
# exact name to keep weight loading a 1:1 key mapping.
|
||
self.qkv = CCA(
|
||
config=config,
|
||
cca_num_k_heads=self.num_k_heads_full,
|
||
cca_num_q_heads=self.num_q_heads_full,
|
||
hidden_size=self.hidden_size,
|
||
head_dim=self.head_dim,
|
||
cca_time0=config.cca_time0,
|
||
cca_time1=config.cca_time1,
|
||
layer_id=layer_id,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("qkv", prefix),
|
||
tp_rank=self.tp_rank,
|
||
tp_size=self.tp_size,
|
||
)
|
||
|
||
# RowParallel o_proj: per-rank input is the rank's q heads, full
|
||
# output is replicated via the end-of-forward all-reduce.
|
||
self.o_proj = RowParallelLinear(
|
||
self.q_dim_full,
|
||
self.hidden_size,
|
||
bias=bool(getattr(config, "attention_bias", False)),
|
||
input_is_parallel=True,
|
||
reduce_results=True,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("o_proj", prefix),
|
||
tp_rank=self.tp_rank,
|
||
tp_size=self.tp_size,
|
||
)
|
||
|
||
rope_theta = float(getattr(config, "rope_theta", 1_000_000.0))
|
||
partial_rotary_factor = float(getattr(config, "partial_rotary_factor", 0.5))
|
||
self.rotary_emb = get_rope(
|
||
head_size=self.head_dim,
|
||
rotary_dim=self.head_dim,
|
||
max_position=int(config.max_position_embeddings),
|
||
base=int(rope_theta),
|
||
is_neox_style=True,
|
||
partial_rotary_factor=partial_rotary_factor,
|
||
)
|
||
|
||
self.attn = RadixAttention(
|
||
num_heads=self.num_q_heads,
|
||
head_dim=self.head_dim,
|
||
scaling=self.scale,
|
||
num_kv_heads=self.num_k_heads,
|
||
layer_id=layer_id,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("attn", prefix),
|
||
)
|
||
|
||
def forward(
|
||
self,
|
||
hidden_states: torch.Tensor,
|
||
positions: torch.Tensor,
|
||
forward_batch: ForwardBatch,
|
||
) -> torch.Tensor:
|
||
# CCA returns fp32 q/k and input-dtype v as ``[T, heads, head_dim]``
|
||
# tensors; flatten the head dim and cast all to the model dtype before
|
||
# rotary + RadixAttention.
|
||
q, k, v = self.qkv(hidden_states, forward_batch)
|
||
target_dtype = hidden_states.dtype
|
||
q = q.reshape(q.shape[0], -1).to(target_dtype)
|
||
k = k.reshape(k.shape[0], -1).to(target_dtype)
|
||
v = v.reshape(v.shape[0], -1).to(target_dtype)
|
||
|
||
q, k = self.rotary_emb(positions, q, k)
|
||
# Some rotary backends (notably AITER on ROCm) hand back tensors with
|
||
# a different stride than the input. RadixAttention's KV-store kernel
|
||
# asserts contiguous layout, so normalize q/k/v before the attention.
|
||
q = q.contiguous()
|
||
k = k.contiguous()
|
||
v = v.contiguous()
|
||
attn_output = self.attn(q, k, v, forward_batch)
|
||
output, _ = self.o_proj(attn_output)
|
||
return output
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Router (EDA + MOD) and MoE block
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class ZayaRouter(nn.Module):
|
||
"""ZAYA1 expert router: 3-layer MLP with optional EDA and MOD.
|
||
|
||
EDA (Exponential Decay Averaging) adds a scaled copy of the previous MoE
|
||
layer's router hidden_state to the current layer's input, threading state
|
||
across MoE layers.
|
||
|
||
MOD (Mixture of Depths) reserves the last expert slot as a "skip" expert
|
||
whose contribution to the residual stream is just the routing probability
|
||
times the unprocessed hidden_state, letting individual tokens bypass the
|
||
MoE entirely when the router scores the skip expert highest.
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
config: ZayaConfig,
|
||
layer_id: int,
|
||
num_moe_experts: int,
|
||
moe_router_topk: int,
|
||
mlp_expansion: int,
|
||
quant_config: Optional[QuantizationConfig] = None,
|
||
prefix: str = "",
|
||
) -> None:
|
||
super().__init__()
|
||
self.config = config
|
||
self.layer_id = layer_id
|
||
self.hidden_size = config.hidden_size
|
||
self.router_softmax_fp32 = bool(getattr(config, "zaya_high_prec", False))
|
||
|
||
self.use_mod = bool(getattr(config, "zaya_use_mod", False))
|
||
self.num_experts = (num_moe_experts + 1) if self.use_mod else num_moe_experts
|
||
self.topk = int(moe_router_topk)
|
||
self.mlp_expansion = int(mlp_expansion)
|
||
|
||
self.down_proj = ReplicatedLinear(
|
||
self.hidden_size,
|
||
self.mlp_expansion,
|
||
bias=True,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("down_proj", prefix),
|
||
)
|
||
|
||
# EDA threads router state from the previous MoE layer through
|
||
# ``router_states_scale``. The first MoE layer in the model has no
|
||
# previous state; whether to fold it in is decided at call time based on
|
||
# ``prev_router_hidden_states``.
|
||
ln_eps = float(getattr(config, "norm_epsilon", 1e-5))
|
||
self.use_eda = bool(getattr(config, "zaya_use_eda", False))
|
||
self.rmsnorm_eda = RMSNorm(self.mlp_expansion, eps=ln_eps)
|
||
if self.use_eda:
|
||
self.router_states_scale = nn.Parameter(torch.ones(self.mlp_expansion))
|
||
|
||
self.non_linearity = nn.GELU()
|
||
self.router_mlp = nn.Sequential(
|
||
ReplicatedLinear(
|
||
self.mlp_expansion,
|
||
self.mlp_expansion,
|
||
bias=True,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("router_mlp.0", prefix),
|
||
),
|
||
self.non_linearity,
|
||
ReplicatedLinear(
|
||
self.mlp_expansion,
|
||
self.mlp_expansion,
|
||
bias=True,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("router_mlp.2", prefix),
|
||
),
|
||
self.non_linearity,
|
||
ReplicatedLinear(
|
||
self.mlp_expansion,
|
||
self.num_experts,
|
||
bias=False,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("router_mlp.4", prefix),
|
||
),
|
||
)
|
||
|
||
self.register_buffer(
|
||
"balancing_biases",
|
||
torch.zeros(self.num_experts, dtype=torch.float32),
|
||
persistent=True,
|
||
)
|
||
if self.use_mod:
|
||
with torch.no_grad():
|
||
self.balancing_biases[-1] = -1.0
|
||
|
||
def forward(
|
||
self,
|
||
hidden_states: torch.Tensor,
|
||
prev_router_hidden_states: Optional[torch.Tensor] = None,
|
||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||
# ``hidden_states`` is ``[T, H]``.
|
||
hs, _ = self.down_proj(hidden_states)
|
||
if (
|
||
self.use_eda
|
||
and prev_router_hidden_states is not None
|
||
and hasattr(self, "router_states_scale")
|
||
):
|
||
hs = hs + prev_router_hidden_states * self.router_states_scale
|
||
|
||
# ``hs`` is a freshly-allocated tensor (output of ``down_proj`` or the
|
||
# EDA add above) and ``rmsnorm_eda`` is non-residual / out-of-place,
|
||
# so we can hand the same buffer to the next layer without cloning.
|
||
router_hidden_states_next = hs
|
||
|
||
hs_norm = self.rmsnorm_eda(hs)
|
||
|
||
# Step through the Sequential manually so the ``(tensor, bias)`` tuple
|
||
# returned by each ReplicatedLinear is unpacked correctly.
|
||
out = hs_norm
|
||
for stage in self.router_mlp:
|
||
if isinstance(stage, ReplicatedLinear):
|
||
out, _ = stage(out)
|
||
else:
|
||
out = stage(out)
|
||
logits = out
|
||
|
||
if self.router_softmax_fp32:
|
||
expert_prob = torch.softmax(logits, dim=-1, dtype=torch.float32)
|
||
else:
|
||
expert_prob = torch.softmax(logits, dim=-1)
|
||
|
||
biased = expert_prob.detach().to(torch.float32) + self.balancing_biases
|
||
_, expert_choice = torch.topk(biased, self.topk, dim=-1)
|
||
|
||
if self.topk > 1 and self.use_mod:
|
||
skip_idx = self.num_experts - 1
|
||
n_mask = expert_choice == skip_idx
|
||
cumsum_mask = torch.cumsum(n_mask, dim=-1)
|
||
expert_choice = expert_choice.masked_fill(cumsum_mask > 0, skip_idx)
|
||
|
||
route_prob = torch.gather(expert_prob, dim=1, index=expert_choice)
|
||
if route_prob.dtype != hidden_states.dtype:
|
||
route_prob = route_prob.to(hidden_states.dtype)
|
||
|
||
return route_prob, expert_choice, router_hidden_states_next
|
||
|
||
|
||
def mod_premask_experts(
|
||
experts_out: torch.Tensor,
|
||
indices: torch.Tensor,
|
||
num_moe_experts: int,
|
||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||
"""Mask the (per-rank, pre-all-reduce) expert output for the MOD skip path.
|
||
|
||
Returns ``(mod_mask, masked_experts)`` where ``mod_mask`` is ``1`` for
|
||
tokens routed to a real expert and ``0`` for tokens routed to the skip
|
||
slot (``indices == num_moe_experts``), and
|
||
``masked_experts = mod_mask * experts_out``.
|
||
|
||
The masking is applied *before* the cross-rank all-reduce so the single
|
||
reduction yields ``mask · sum_r(partial_r) = mask · experts_out_full``
|
||
without the replicated ``mod_out`` term being summed ``tp_size`` times.
|
||
Pairs with :func:`mod_blend`, which adds the skip-path term back after the
|
||
reduce. Kept as a free function so the MOD math is unit-testable without a
|
||
live ``torch.distributed`` group.
|
||
"""
|
||
mod_mask = (indices != num_moe_experts).to(experts_out.dtype)
|
||
return mod_mask, mod_mask * experts_out
|
||
|
||
|
||
def mod_blend(
|
||
masked_experts_reduced: torch.Tensor,
|
||
mod_mask: torch.Tensor,
|
||
mod_out: torch.Tensor,
|
||
) -> torch.Tensor:
|
||
"""Combine the already-all-reduced masked expert output with the skip path.
|
||
|
||
``mod_out`` (the skip-expert residual, ``hidden_states * prob``) is
|
||
replicated on every rank, so it is folded in here -- after the reduce of
|
||
``masked_experts`` -- weighted by ``(1 - mod_mask)``. See
|
||
:func:`mod_premask_experts`.
|
||
"""
|
||
return masked_experts_reduced + (1.0 - mod_mask) * mod_out
|
||
|
||
|
||
class ZayaBlock(nn.Module):
|
||
"""ZAYA1 MoE mixer: ZayaRouter feeding FusedMoE, with optional MOD residual blend."""
|
||
|
||
def __init__(
|
||
self,
|
||
config: ZayaConfig,
|
||
layer_id: int,
|
||
quant_config: Optional[QuantizationConfig] = None,
|
||
prefix: str = "",
|
||
) -> None:
|
||
super().__init__()
|
||
self.config = config
|
||
self.layer_id = layer_id
|
||
self.num_moe_experts = int(config.num_experts)
|
||
self.mlp_expansion = int(config.zaya_mlp_expansion)
|
||
self.topk = int(getattr(config, "moe_router_topk", 1))
|
||
|
||
self.tp_size = get_parallel().tp_size
|
||
if self.tp_size > self.num_moe_experts:
|
||
raise ValueError(
|
||
f"Tensor parallel size {self.tp_size} is greater than the "
|
||
f"number of experts {self.num_moe_experts}"
|
||
)
|
||
|
||
assert (
|
||
config.activation_func == "swiglu"
|
||
), "ZayaBlock only supports SwiGLU activation"
|
||
assert config.gated_linear_unit, "ZayaBlock requires gated_linear_unit=True"
|
||
|
||
self.router = ZayaRouter(
|
||
config=config,
|
||
layer_id=layer_id,
|
||
num_moe_experts=self.num_moe_experts,
|
||
moe_router_topk=self.topk,
|
||
mlp_expansion=self.mlp_expansion,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("router", prefix),
|
||
)
|
||
|
||
# ffn_hidden_size is the merged (gate+up) hidden dim; the per-side
|
||
# intermediate is half.
|
||
intermediate = int(config.ffn_hidden_size) // 2
|
||
self.experts = get_moe_impl_class(quant_config)(
|
||
num_experts=self.num_moe_experts,
|
||
top_k=self.topk,
|
||
hidden_size=config.hidden_size,
|
||
intermediate_size=intermediate,
|
||
layer_id=layer_id,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("experts", prefix),
|
||
activation="silu",
|
||
reduce_results=False,
|
||
)
|
||
|
||
def forward(
|
||
self,
|
||
hidden_states: torch.Tensor,
|
||
prev_router_hidden_states: Optional[torch.Tensor] = None,
|
||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||
if hidden_states.shape[0] == 0:
|
||
return hidden_states, hidden_states.new_zeros((0, self.mlp_expansion))
|
||
|
||
probs, indices, router_hs_next = self.router(
|
||
hidden_states, prev_router_hidden_states
|
||
)
|
||
|
||
topk_out = StandardTopKOutput(
|
||
topk_weights=probs.to(hidden_states.dtype),
|
||
topk_ids=indices.to(torch.int32),
|
||
router_logits=probs.to(hidden_states.dtype),
|
||
)
|
||
|
||
if self.config.zaya_use_mod:
|
||
# MOD: clamp the "skip expert" id (== num_moe_experts) into the
|
||
# valid expert range so FusedMoE never indexes out of bounds; the
|
||
# mask below decides per-token whether to actually use experts or
|
||
# the skip path.
|
||
clamped_ids = torch.clamp(indices, min=0, max=self.num_moe_experts - 1).to(
|
||
torch.int32
|
||
)
|
||
topk_out = topk_out._replace(topk_ids=clamped_ids)
|
||
|
||
experts_out = self.experts(hidden_states, topk_out)
|
||
# ``mod_out`` is computed identically on every TP rank (both
|
||
# ``hidden_states`` and ``probs`` are replicated). Fold the skip
|
||
# mask into the per-rank partial experts output *before*
|
||
# all-reduce so the single reduction yields:
|
||
# sum_r(mask · partial_r) + (1 - mask) · mod_out
|
||
# = mask · experts_out_full + (1 - mask) · mod_out
|
||
# without double-counting ``mod_out`` by tp_size. The two steps are
|
||
# ``mod_premask_experts`` / ``mod_blend`` so the math is testable
|
||
# without a live distributed group.
|
||
mod_out = hidden_states * probs
|
||
mod_mask, masked_experts = mod_premask_experts(
|
||
experts_out, indices, self.num_moe_experts
|
||
)
|
||
if self.tp_size > 1:
|
||
masked_experts = tensor_model_parallel_all_reduce(masked_experts)
|
||
hidden_out = mod_blend(masked_experts, mod_mask, mod_out)
|
||
else:
|
||
hidden_out = self.experts(hidden_states, topk_out)
|
||
if self.tp_size > 1:
|
||
hidden_out = tensor_model_parallel_all_reduce(hidden_out)
|
||
|
||
return hidden_out, router_hs_next
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Decoder layers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class ZayaDecoderATTLayer(nn.Module):
|
||
"""Attention decoder layer: ``res_scale → input_norm → ZayaAttention``."""
|
||
|
||
def __init__(
|
||
self,
|
||
config: ZayaConfig,
|
||
layer_id: int,
|
||
quant_config: Optional[QuantizationConfig] = None,
|
||
prefix: str = "",
|
||
) -> None:
|
||
super().__init__()
|
||
self.config = config
|
||
self.layer_id = layer_id
|
||
|
||
self.self_attn = ZayaAttention(
|
||
config=config,
|
||
layer_id=layer_id,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("self_attn", prefix),
|
||
)
|
||
self.input_norm = self._build_norm(config)
|
||
if config.scale_residual_merge:
|
||
self.res_scale = ResidualScaling(config, layer_id)
|
||
else:
|
||
self.res_scale = None
|
||
|
||
@staticmethod
|
||
def _build_norm(config: ZayaConfig) -> nn.Module:
|
||
if config.normalization == "RMSNorm":
|
||
return RMSNorm(config.hidden_size, eps=config.norm_epsilon)
|
||
if config.normalization == "LayerNorm":
|
||
return nn.LayerNorm(config.hidden_size, eps=config.norm_epsilon)
|
||
raise ValueError(f"Unsupported normalization: {config.normalization}")
|
||
|
||
def forward(
|
||
self,
|
||
hidden_states: torch.Tensor,
|
||
residual: Optional[torch.Tensor],
|
||
positions: torch.Tensor,
|
||
forward_batch: ForwardBatch,
|
||
prev_router_hidden_states: Optional[torch.Tensor] = None,
|
||
) -> tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
|
||
target_dtype = (
|
||
self.input_norm.weight.dtype
|
||
if isinstance(self.input_norm, RMSNorm)
|
||
else hidden_states.dtype
|
||
)
|
||
if self.res_scale is not None:
|
||
residual, hidden_states = self.res_scale(residual, hidden_states)
|
||
if residual is not None:
|
||
residual = residual.float() + hidden_states.float()
|
||
else:
|
||
residual = hidden_states.float()
|
||
hidden_states = _apply_norm_with_fp32_residual(
|
||
self.input_norm, residual, target_dtype
|
||
)
|
||
hidden_states = self.self_attn(hidden_states, positions, forward_batch)
|
||
return hidden_states, residual, prev_router_hidden_states
|
||
|
||
|
||
class ZayaDecoderMLPLayer(nn.Module):
|
||
"""MoE decoder layer: ``res_scale → input_norm → ZayaBlock``."""
|
||
|
||
def __init__(
|
||
self,
|
||
config: ZayaConfig,
|
||
layer_id: int,
|
||
quant_config: Optional[QuantizationConfig] = None,
|
||
prefix: str = "",
|
||
) -> None:
|
||
super().__init__()
|
||
self.config = config
|
||
self.layer_id = layer_id
|
||
|
||
self.zaya_block = ZayaBlock(
|
||
config=config,
|
||
layer_id=layer_id,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("zaya_block", prefix),
|
||
)
|
||
self.input_norm = ZayaDecoderATTLayer._build_norm(config)
|
||
if config.scale_residual_merge:
|
||
self.res_scale = ResidualScaling(config, layer_id)
|
||
else:
|
||
self.res_scale = None
|
||
|
||
def forward(
|
||
self,
|
||
hidden_states: torch.Tensor,
|
||
residual: Optional[torch.Tensor],
|
||
positions: torch.Tensor,
|
||
forward_batch: ForwardBatch,
|
||
prev_router_hidden_states: Optional[torch.Tensor] = None,
|
||
) -> tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
|
||
target_dtype = (
|
||
self.input_norm.weight.dtype
|
||
if isinstance(self.input_norm, RMSNorm)
|
||
else hidden_states.dtype
|
||
)
|
||
if self.res_scale is not None:
|
||
residual, hidden_states = self.res_scale(residual, hidden_states)
|
||
if residual is not None:
|
||
residual = residual.float() + hidden_states.float()
|
||
else:
|
||
residual = hidden_states.float()
|
||
hidden_states = _apply_norm_with_fp32_residual(
|
||
self.input_norm, residual, target_dtype
|
||
)
|
||
hidden_states, prev_router_hidden_states = self.zaya_block(
|
||
hidden_states, prev_router_hidden_states
|
||
)
|
||
return hidden_states, residual, prev_router_hidden_states
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Top-level model
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _build_layer(
|
||
layer_id: int,
|
||
config: ZayaConfig,
|
||
quant_config: Optional[QuantizationConfig],
|
||
prefix: str,
|
||
) -> nn.Module:
|
||
# Even layer ids are attention, odd layer ids are MoE. This matches the HF
|
||
# checkpoint keys: ``model.layers.<2k>.self_attn.*`` (CCA) versus
|
||
# ``model.layers.<2k+1>.zaya_block.*`` (MoE).
|
||
if layer_id % 2 == 0:
|
||
return ZayaDecoderATTLayer(
|
||
config=config,
|
||
layer_id=layer_id,
|
||
quant_config=quant_config,
|
||
prefix=prefix,
|
||
)
|
||
return ZayaDecoderMLPLayer(
|
||
config=config,
|
||
layer_id=layer_id,
|
||
quant_config=quant_config,
|
||
prefix=prefix,
|
||
)
|
||
|
||
|
||
class ZayaModel(nn.Module):
|
||
def __init__(
|
||
self,
|
||
config: ZayaConfig,
|
||
quant_config: Optional[QuantizationConfig] = None,
|
||
prefix: str = "",
|
||
) -> None:
|
||
super().__init__()
|
||
self.config = config
|
||
self.padding_idx = config.pad_token_id
|
||
self.vocab_size = config.vocab_size
|
||
self.pp_group = get_pp_group()
|
||
|
||
if self.pp_group.is_first_rank:
|
||
self.embed_tokens = VocabParallelEmbedding(
|
||
config.vocab_size,
|
||
config.hidden_size,
|
||
org_num_embeddings=config.vocab_size,
|
||
prefix=add_prefix("embed_tokens", prefix),
|
||
)
|
||
else:
|
||
self.embed_tokens = PPMissingLayer()
|
||
|
||
self.layers, self.start_layer, self.end_layer = make_layers(
|
||
config.num_hidden_layers,
|
||
lambda idx, prefix: _build_layer(
|
||
layer_id=idx,
|
||
config=config,
|
||
quant_config=quant_config,
|
||
prefix=prefix,
|
||
),
|
||
pp_rank=self.pp_group.rank_in_group,
|
||
pp_size=self.pp_group.world_size,
|
||
prefix=add_prefix("layers", prefix),
|
||
)
|
||
|
||
if self.pp_group.is_last_rank:
|
||
self.final_norm = ZayaDecoderATTLayer._build_norm(config)
|
||
if config.scale_residual_merge:
|
||
self.res_scale = ResidualScaling(config, config.num_hidden_layers)
|
||
else:
|
||
self.res_scale = None
|
||
else:
|
||
self.final_norm = PPMissingLayer()
|
||
self.res_scale = None
|
||
|
||
def forward(
|
||
self,
|
||
input_ids: torch.Tensor,
|
||
positions: torch.Tensor,
|
||
forward_batch: ForwardBatch,
|
||
inputs_embeds: Optional[torch.Tensor] = None,
|
||
pp_proxy_tensors: Optional[PPProxyTensors] = None,
|
||
) -> torch.Tensor:
|
||
if self.pp_group.is_first_rank:
|
||
if inputs_embeds is not None:
|
||
hidden_states = inputs_embeds
|
||
else:
|
||
hidden_states = self.embed_tokens(input_ids)
|
||
residual = None
|
||
else:
|
||
assert pp_proxy_tensors is not None
|
||
hidden_states = pp_proxy_tensors["hidden_states"]
|
||
residual = pp_proxy_tensors["residual"]
|
||
|
||
prev_router_hidden_states: Optional[torch.Tensor] = None
|
||
for i in range(self.start_layer, self.end_layer):
|
||
layer = self.layers[i]
|
||
hidden_states, residual, prev_router_hidden_states = layer(
|
||
hidden_states=hidden_states,
|
||
residual=residual,
|
||
positions=positions,
|
||
forward_batch=forward_batch,
|
||
prev_router_hidden_states=prev_router_hidden_states,
|
||
)
|
||
|
||
if not self.pp_group.is_last_rank:
|
||
return PPProxyTensors(
|
||
{
|
||
"hidden_states": hidden_states,
|
||
"residual": residual,
|
||
}
|
||
)
|
||
|
||
if self.res_scale is not None:
|
||
residual, hidden_states = self.res_scale(residual, hidden_states)
|
||
target_dtype = (
|
||
self.final_norm.weight.dtype
|
||
if isinstance(self.final_norm, RMSNorm)
|
||
else hidden_states.dtype
|
||
)
|
||
if residual is not None:
|
||
merged = hidden_states.float() + residual.float()
|
||
else:
|
||
merged = hidden_states.float()
|
||
hidden_states = _apply_norm_with_fp32_residual(
|
||
self.final_norm, merged, target_dtype
|
||
)
|
||
return hidden_states
|
||
|
||
|
||
class ZayaForCausalLM(nn.Module):
|
||
def __init__(
|
||
self,
|
||
config: ZayaConfig,
|
||
quant_config: Optional[QuantizationConfig] = None,
|
||
prefix: str = "",
|
||
) -> None:
|
||
super().__init__()
|
||
self.config = config
|
||
self.quant_config = quant_config
|
||
self.pp_group = get_pp_group()
|
||
|
||
self.model = ZayaModel(
|
||
config=config,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("model", prefix),
|
||
)
|
||
|
||
if self.pp_group.is_last_rank:
|
||
self.lm_head = ParallelLMHead(
|
||
config.vocab_size,
|
||
config.hidden_size,
|
||
org_num_embeddings=config.vocab_size,
|
||
bias=bool(getattr(config, "lm_head_bias", False)),
|
||
quant_config=None,
|
||
prefix=add_prefix("lm_head", prefix),
|
||
)
|
||
if config.tie_word_embeddings:
|
||
self.lm_head = self.lm_head.tie_weights(self.model.embed_tokens)
|
||
else:
|
||
self.lm_head = PPMissingLayer()
|
||
|
||
self.logits_processor = LogitsProcessor(config)
|
||
|
||
@torch.no_grad()
|
||
def forward(
|
||
self,
|
||
input_ids: torch.Tensor,
|
||
positions: torch.Tensor,
|
||
forward_batch: ForwardBatch,
|
||
inputs_embeds: Optional[torch.Tensor] = None,
|
||
pp_proxy_tensors: Optional[PPProxyTensors] = None,
|
||
) -> torch.Tensor:
|
||
hidden_states = self.model(
|
||
input_ids=input_ids,
|
||
positions=positions,
|
||
forward_batch=forward_batch,
|
||
inputs_embeds=inputs_embeds,
|
||
pp_proxy_tensors=pp_proxy_tensors,
|
||
)
|
||
if not self.pp_group.is_last_rank:
|
||
return hidden_states
|
||
return self.logits_processor(
|
||
input_ids, hidden_states, self.lm_head, forward_batch
|
||
)
|
||
|
||
# ---------------- weight loading ----------------
|
||
|
||
_EXPERT_RE = re.compile(
|
||
r"^(.*\.zaya_block\.experts)\.local_experts\.(\d+)\.(linear_fc1|linear_fc2)\.weight$"
|
||
)
|
||
|
||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||
"""Load an HF ZAYA1 safetensors checkpoint into the SGLang module tree.
|
||
|
||
Most keys map 1:1 because the module names already mirror the HF
|
||
checkpoint layout. Two cases need rewriting:
|
||
|
||
1. ``self_attn.qkv.{linear_q, linear_k, conv_qk.{0,1}, val_proj{1,2}, temp}``
|
||
lands directly on the registered ``CCA`` submodule (which is named
|
||
``qkv`` exactly to keep this mapping trivial).
|
||
2. ``zaya_block.experts.local_experts.<i>.linear_fc1.weight`` (gate
|
||
and up projections concatenated along dim 0) is split and routed
|
||
to FusedMoE shards ``w1`` (first half) and ``w3`` (second half);
|
||
``linear_fc2.weight`` becomes the FusedMoE ``w2`` shard.
|
||
"""
|
||
params_dict = dict(self.named_parameters())
|
||
buffers_dict = dict(self.named_buffers())
|
||
# ``balancing_biases`` is a persistent buffer; FusedMoE may also expose
|
||
# buffers. Expose them all through ``params_dict`` so that the regular
|
||
# ``default_weight_loader`` can write to them.
|
||
for key, buf in buffers_dict.items():
|
||
params_dict.setdefault(key, buf)
|
||
|
||
fused_moe_modules: dict[str, nn.Module] = {}
|
||
for name, module in self.named_modules():
|
||
if module.__class__.__name__ == "FusedMoE" or hasattr(module, "w13_weight"):
|
||
fused_moe_modules[name] = module
|
||
|
||
loaded_params: set[str] = set()
|
||
|
||
for ckpt_name, loaded_weight in weights:
|
||
# Skip keys that have no runtime counterpart in this model.
|
||
if ckpt_name.startswith("lm_head") and self.config.tie_word_embeddings:
|
||
continue
|
||
if "rotary_emb" in ckpt_name:
|
||
continue
|
||
|
||
match = self._EXPERT_RE.match(ckpt_name)
|
||
if match is not None:
|
||
experts_prefix = match.group(
|
||
1
|
||
) # e.g. model.layers.1.zaya_block.experts
|
||
expert_id = int(match.group(2))
|
||
kind = match.group(3)
|
||
moe_module = fused_moe_modules.get(experts_prefix)
|
||
if moe_module is None:
|
||
logger.warning(
|
||
"FusedMoE module %s not found; skipping %s",
|
||
experts_prefix,
|
||
ckpt_name,
|
||
)
|
||
continue
|
||
weight_loader = moe_module.weight_loader
|
||
if kind == "linear_fc1":
|
||
param_name = f"{experts_prefix}.w13_weight"
|
||
param = params_dict.get(param_name)
|
||
if param is None:
|
||
logger.warning("No param %s for %s", param_name, ckpt_name)
|
||
continue
|
||
half = loaded_weight.shape[0] // 2
|
||
weight_loader(
|
||
param,
|
||
loaded_weight[:half],
|
||
ckpt_name,
|
||
shard_id="w1",
|
||
expert_id=expert_id,
|
||
)
|
||
weight_loader(
|
||
param,
|
||
loaded_weight[half:],
|
||
ckpt_name,
|
||
shard_id="w3",
|
||
expert_id=expert_id,
|
||
)
|
||
loaded_params.add(param_name)
|
||
else: # linear_fc2
|
||
param_name = f"{experts_prefix}.w2_weight"
|
||
param = params_dict.get(param_name)
|
||
if param is None:
|
||
logger.warning("No param %s for %s", param_name, ckpt_name)
|
||
continue
|
||
weight_loader(
|
||
param,
|
||
loaded_weight,
|
||
ckpt_name,
|
||
shard_id="w2",
|
||
expert_id=expert_id,
|
||
)
|
||
loaded_params.add(param_name)
|
||
continue
|
||
|
||
# HF stores CCA tensors under ``self_attn.qkv.*``, which already
|
||
# matches our submodule registration, so no rename is needed.
|
||
if ckpt_name not in params_dict:
|
||
# ``conv_qk`` is an ``nn.Sequential`` of two ``nn.Conv1d``,
|
||
# whose keys end in ``.0.{weight,bias}`` / ``.1.{weight,bias}``
|
||
# and are exposed through ``named_parameters()`` automatically.
|
||
# Anything else is genuinely unknown – warn and skip.
|
||
logger.warning(
|
||
"WARNING: checkpoint key %s has no matching parameter; skipping",
|
||
ckpt_name,
|
||
)
|
||
continue
|
||
|
||
param = params_dict[ckpt_name]
|
||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||
weight_loader(param, loaded_weight)
|
||
loaded_params.add(ckpt_name)
|
||
|
||
return loaded_params
|
||
|
||
|
||
EntryClass = ZayaForCausalLM
|