[CPU] Add support for Gemma4 on Xeon (#22498)

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jianan-gu <jianan.gu@intel.com>
Co-authored-by: Haotong Zou <haotong.zou@intel.com>
This commit is contained in:
blzheng
2026-08-17 10:52:26 +08:00
committed by GitHub
co-authored by Copilot jianan-gu Haotong Zou
parent 3adc70bb5e
commit b6d7602914
27 changed files with 514 additions and 106 deletions
@@ -242,7 +242,8 @@ at::Tensor fused_experts_cpu(
const std::optional<at::Tensor>& /*w2_bias*/,
const std::optional<double>& /*alpha*/,
const std::optional<double>& /*limit*/,
bool /*is_vnni*/) {
bool /*is_vnni*/,
const std::optional<std::string>& activation) {
const auto st = hidden_states.scalar_type();
CHECK_INPUT(hidden_states);
CHECK_INPUT(w13);
@@ -256,6 +257,10 @@ at::Tensor fused_experts_cpu(
CHECK_EQ(topk_ids.scalar_type(), at::kInt);
if (activation.has_value() && activation.value() != "silu") {
TORCH_CHECK(false, "fused_experts_cpu on ARM64 only supports activation='silu', got: ", activation.value());
}
// TODO: support topk_weights to be bf16 or fp16 in the kernel
auto topk_weights_ = topk_weights.to(at::kFloat);
@@ -167,7 +167,7 @@ at::Tensor gelu_and_mul_cpu(const at::Tensor& input) {
num_tokens,
d,
[inv_sqrt2](float x) { return 0.5f * x * (1.f + std::erf(x * inv_sqrt2)); },
[inv_sqrt2](Vec x) { return Vec(0.5f) * x * (Vec(1.f) + (x * Vec(inv_sqrt2)).erf()); });
[](Vec x) { return fast_gelu(x); });
});
return out;
+35 -12
View File
@@ -63,6 +63,7 @@ void extend_attention_kernel_impl(
int64_t sliding_window_size,
bool is_prefix_skipped,
bool is_cross_attn,
bool kv_from_cache,
bool has_encoder_lens,
bool has_sink) {
// strides
@@ -148,8 +149,9 @@ void extend_attention_kernel_impl(
fill_stub(s_prime, 0.f, m_size);
fill_stub(m_prime, -std::numeric_limits<scalar_t>::infinity(), m_size);
// stage 1: compute scores with prefix
// kv_from_cache has no stage 2, so cover the extend range and stop at the diagonal
int kv_start = 0;
int kv_end = is_cross_attn ? encoder_lens[bs] : seq_len_prefix;
int kv_end = is_cross_attn ? encoder_lens[bs] : (kv_from_cache ? seq_len_prefix + m + m_size : seq_len_prefix);
for (int n = kv_start; n < kv_end; n += BLOCK_N) {
int n_size = std::min(BLOCK_N, kv_end - n);
@@ -180,12 +182,31 @@ void extend_attention_kernel_impl(
/* C */ s_i);
for (int row = 0; row < m_size; ++row) {
if (sliding_window_size > 0) {
bool row_is_empty = false;
if (kv_from_cache) {
int first_future_col = seq_len_prefix + m + row + 1;
if (n >= first_future_col) {
row_is_empty = true;
} else if (first_future_col < n + n_size) {
fill_stub(
s_i + row * BLOCK_N + (first_future_col - n),
-std::numeric_limits<float>::infinity(),
n + n_size - first_future_col);
}
}
if (!row_is_empty && sliding_window_size > 0) {
int last_col = seq_len_prefix + row + m - sliding_window_size + 1;
if (last_col >= n + n_size) {
continue;
row_is_empty = true;
} else {
fill_stub(s_i + row * BLOCK_N, -std::numeric_limits<float>::infinity(), last_col - n);
}
fill_stub(s_i + row * BLOCK_N, -std::numeric_limits<float>::infinity(), last_col - n);
}
if (row_is_empty) {
// s_delta is reused across blocks - zero an empty row rather than
// skip it, or P @ V below applies the previous block's weights here
fill_stub(s_delta + row * BLOCK_N, 0.f, padded_n_size);
continue;
}
flash_attn_softmax<scalar_t, BLOCK_M, BLOCK_N>::apply(
s_i, s_delta, v_prime, s_prime, m_prime, m_size, n_size, padded_n_size, head_size_v, sm_scale, row);
@@ -214,7 +235,7 @@ void extend_attention_kernel_impl(
/* B */ Btmp,
/* C */ v_prime);
} // loop with seq_len_prefix
if (!is_cross_attn) {
if (!is_cross_attn && !kv_from_cache) {
// stage 2: compute the triangle part
int num_keys = std::min(seq_len_extend, m + BLOCK_M);
for (int n = 0; n < num_keys; n += BLOCK_N) {
@@ -400,6 +421,7 @@ inline int resize_buffer(at::Tensor& buffer, int num_threads, int head_size, int
sliding_window_size, \
is_prefix_skipped, \
is_cross_attn, \
kv_from_cache, \
has_encoder_lens, \
has_sink); \
} while (0)
@@ -442,13 +464,13 @@ void extend_attention_cpu(
std::optional<at::Tensor> encoder_lens,
std::optional<at::Tensor> sinks,
std::optional<at::Tensor> tree_mask) {
if (!is_cross_attn) {
TORCH_CHECK(
k_extend_opt.has_value() && v_extend_opt.has_value(),
"k_extend and v_extend are required for non-cross attention");
}
// Since k_extend and v_extend are not used for cross attention, they can be initialized as k_buffer and v_buffer
// here.
TORCH_CHECK(k_extend_opt.has_value() == v_extend_opt.has_value(), "k_extend and v_extend must be given together");
// A KV-shared layer (Gemma 4) passes no extend K/V - the layer it shares with
// already wrote them to the cache, so this kernel masks causally itself. Cross
// attention can also arrive without K/V but reads an encoder sequence that
// carries no causal order, so it keeps its own path.
const bool kv_from_cache = !is_cross_attn && !k_extend_opt.has_value();
// unused when the range comes from the cache - bind them to the buffers
auto k_extend = k_extend_opt.has_value() ? k_extend_opt.value() : k_buffer;
auto v_extend = v_extend_opt.has_value() ? v_extend_opt.value() : v_buffer;
@@ -540,6 +562,7 @@ void extend_attention_cpu(
", got ",
tree_mask_t.numel());
TORCH_CHECK(!is_cross_attn, "extend: tree_mask is not supported for cross attention");
TORCH_CHECK(!kv_from_cache, "extend: tree_mask is not supported for KV-shared layers");
// The window mask derives query positions from the row index
// (seq_len_prefix + m + row), but tree-mask rows sit at their tree depth,
// which is <= the row index; combining the two would over-mask the prefix.
+20
View File
@@ -73,6 +73,26 @@ enum class CPUActMethod : int {
gelu_and_mul = 2,
};
// swiglu is selected by its (alpha, limit) parameters rather than by name and
// takes precedence; the name is still validated, so it cannot pass silently.
inline CPUActMethod act_method_from_string(const std::optional<std::string>& activation, bool has_swiglu_params) {
const bool unnamed_or_silu = !activation.has_value() || activation.value() == "silu";
if (has_swiglu_params) {
TORCH_CHECK(
unnamed_or_silu || activation.value() == "swiglu",
"Unsupported activation with clamped swiglu parameters: ",
activation.value());
return CPUActMethod::swiglu;
}
if (unnamed_or_silu) {
return CPUActMethod::silu_and_mul;
}
if (activation.value() == "gelu") {
return CPUActMethod::gelu_and_mul;
}
TORCH_CHECK(false, "Unsupported activation: ", activation.value(), ". Supported: silu, gelu");
}
enum class CPUQuantMethod : int64_t { BF16 = 0, INT8_W8A8 = 1, FP8_W8A16 = 2, INT4_W4A8 = 3, MXFP4 = 4 };
constexpr bool operator==(CPUQuantMethod a, int64_t b) {
+19 -6
View File
@@ -497,7 +497,8 @@ void fused_experts_kernel_impl(
} else {
const int64_t offset = offsets[mb];
if (act_func == CPUActMethod::swiglu) {
// the fused path below hardcodes SiLU in its store step
if (act_func != CPUActMethod::silu_and_mul) {
tinygemm_kernel(
/* A */ A,
/* B */ B0,
@@ -539,12 +540,16 @@ void fused_experts_kernel_impl(
add_bias_stub(C1 + m * BLOCK_N, B1_bias, n_size);
}
}
// 1.d silu and mul
// 1.d activation and mul
const int64_t offset = offsets[mb];
if (act_func == CPUActMethod::silu_and_mul && use_brgemm) {
for (int64_t m = 0; m < m_size; ++m) {
silu_and_mul_stub(ic1 + (offset + m) * N + nb * BLOCK_N, C0 + m * BLOCK_N, C1 + m * BLOCK_N, BLOCK_N);
}
} else if (act_func == CPUActMethod::gelu_and_mul) {
for (int64_t m = 0; m < m_size; ++m) {
gelu_and_mul_stub(ic1 + (offset + m) * N + nb * BLOCK_N, C0 + m * BLOCK_N, C1 + m * BLOCK_N, BLOCK_N);
}
} else if (act_func == CPUActMethod::swiglu) {
for (int64_t m = 0; m < m_size; ++m) {
scalar_t* __restrict__ ic1_row = ic1 + (offset + m) * N;
@@ -883,7 +888,17 @@ at::Tensor fused_experts_cpu(
const std::optional<at::Tensor>& w2_bias,
const std::optional<double>& alpha,
const std::optional<double>& limit,
bool is_vnni) {
bool is_vnni,
const std::optional<std::string>& activation) {
const CPUActMethod act_func = act_method_from_string(activation, alpha.has_value() && limit.has_value());
// the int8 and int4 kernels hardcode silu in their fused store step
const bool is_int_quant =
moe_comp_method == CPUQuantMethod::INT8_W8A8 || moe_comp_method == CPUQuantMethod::INT4_W4A8;
TORCH_CHECK(
!is_int_quant || act_func == CPUActMethod::silu_and_mul,
"fused_experts_cpu: INT8_W8A8 and INT4_W4A8 support activation='silu' only, got: ",
activation.value_or("silu"));
auto packed_w1 = is_vnni ? w1 : convert_weight_packed(w1);
auto packed_w2 = is_vnni ? w2 : convert_weight_packed(w2);
@@ -909,6 +924,7 @@ at::Tensor fused_experts_cpu(
}
CHECK_DIM(2, topk_weights);
CHECK_DIM(2, topk_ids_);
CHECK_EQ(topk_ids_.scalar_type(), at::kInt);
// TODO: support topk_weights to be bf16 or fp16 in the kernel.
@@ -1055,7 +1071,6 @@ at::Tensor fused_experts_cpu(
scalar_t* __restrict__ intermediate_cache0 = (scalar_t*)((void*)(C_tmp + num_threads * 2 * BLOCK_M * BLOCK_N));
scalar_t* __restrict__ B_tmp = (scalar_t*)((void*)(intermediate_cache0 + M * topk * 2 * N));
bool with_bias = w1_bias.has_value();
auto act_func = alpha.has_value() && limit.has_value() ? CPUActMethod::swiglu : CPUActMethod::silu_and_mul;
CHECK_MOE_SCALES_FP8(1, 2);
fused_experts_fp_kernel_impl<scalar_t, at::Float8_e4m3fn, float, false>(
@@ -1095,7 +1110,6 @@ at::Tensor fused_experts_cpu(
scalar_t* __restrict__ intermediate_cache0 = (scalar_t*)((void*)(C_tmp + num_threads * 2 * BLOCK_M * BLOCK_N));
scalar_t* __restrict__ B_tmp = (scalar_t*)((void*)(intermediate_cache0 + M * topk * 2 * N));
bool with_bias = w1_bias.has_value();
auto act_func = alpha.has_value() && limit.has_value() ? CPUActMethod::swiglu : CPUActMethod::silu_and_mul;
// mxfp4 supports only group size of 32 (2^5)
constexpr int64_t group_size = 32;
@@ -1180,7 +1194,6 @@ at::Tensor fused_experts_cpu(
scalar_t* __restrict__ A_tmp = intermediate_cache2 + M * topk * K;
float* __restrict__ C_tmp = (float*)((void*)(A_tmp + num_threads * BLOCK_M * K));
bool with_bias = w1_bias.has_value();
auto act_func = alpha.has_value() && limit.has_value() ? CPUActMethod::swiglu : CPUActMethod::silu_and_mul;
fused_experts_kernel_impl<scalar_t>(
out_hidden_states.data_ptr<scalar_t>(),
+20
View File
@@ -159,6 +159,26 @@ inline void silu_and_mul_stub(
}
}
template <typename scalar_t, typename input_t>
inline void gelu_and_mul_stub(
scalar_t* __restrict__ out, const input_t* __restrict__ input, const input_t* __restrict__ input2, int64_t size) {
static_assert(
std::is_same_v<input_t, float> || std::is_same_v<input_t, scalar_t>,
"gelu_and_mul_stub only supports input_t == float or input_t == scalar_t");
using bVec = at::vec::Vectorized<scalar_t>;
// no remainder
#pragma GCC unroll 4
for (int64_t d = 0; d < size; d += bVec::size()) {
auto [x0, x1] = load_float_vec2(input + d);
auto [y0, y1] = load_float_vec2(input2 + d);
x0 = fast_gelu(x0) * y0;
x1 = fast_gelu(x1) * y1;
bVec out_vec = convert_from_float_ext<scalar_t>(x0, x1);
out_vec.store(out + d);
}
}
template <typename scalar_t, typename input_t>
inline void clamp_sigmoid_and_mul_stub(
scalar_t* __restrict__ out, const input_t* __restrict__ input, int64_t size, const float alpha, const float limit) {
@@ -116,13 +116,19 @@ void fused_experts_fp_kernel_impl(
}
});
// stage 1.5: intermediate_cache1 = silu(intermediate_cache0)
// stage 1.5: intermediate_cache1 = activation(intermediate_cache0)
if (act_func == CPUActMethod::silu_and_mul) {
at::parallel_for(0, M * topk, 0, [&](int64_t begin, int64_t end) {
for (int64_t m = begin; m < end; ++m) {
silu_and_mul_stub(ic1 + m * N, ic0 + m * 2 * N, ic0 + m * 2 * N + N, N);
}
});
} else if (act_func == CPUActMethod::gelu_and_mul) {
at::parallel_for(0, M * topk, 0, [&](int64_t begin, int64_t end) {
for (int64_t m = begin; m < end; ++m) {
gelu_and_mul_stub(ic1 + m * N, ic0 + m * 2 * N, ic0 + m * 2 * N + N, N);
}
});
} else if (act_func == CPUActMethod::swiglu) {
at::parallel_for(0, M * topk, 0, [&](int64_t begin, int64_t end) {
for (int64_t m = begin; m < end; ++m) {
+42 -2
View File
@@ -265,6 +265,47 @@ void rotary_embedding_kernel_impl(
} // namespace
// query: [num_tokens, num_heads, head_dim]
// key: [num_tokens, num_heads, head_dim]
// cos: [num_tokens, head_dim]
// sin: [num_tokens, head_dim]
// Gemma 4's vision tower rotates ndim = 2 head_dim chunks independently, so
// this is apply_rotary_pos_emb_cpu run once per chunk, in place.
void apply_multidimensional_rope_cpu(at::Tensor& query, at::Tensor& key, at::Tensor& cos, at::Tensor& sin) {
CHECK_DIM(3, query);
const auto input_dtype = query.scalar_type();
int64_t num_tokens = query.size(0);
int64_t num_heads = query.size(1);
int64_t head_size = query.size(2);
CHECK_LAST_DIM_CONTIGUOUS_INPUT(query);
CHECK_INPUT_SHAPE_DTYPE<true>(key, {num_tokens, num_heads, head_size}, input_dtype);
CHECK_INPUT_SHAPE_DTYPE<false>(cos, {num_tokens, head_size}, cos.scalar_type());
CHECK_INPUT_SHAPE_DTYPE<false>(sin, {num_tokens, head_size}, sin.scalar_type());
CHECK_EQ(cos.scalar_type(), sin.scalar_type());
constexpr int64_t ndim = 2;
TORCH_CHECK(
head_size % (2 * ndim) == 0, "head_size must be divisible by ", 2 * ndim, " for ndim = ", ndim, " rotary chunks");
const int64_t chunk_size = head_size / ndim;
const RopeParams p{query, key, head_size, chunk_size};
CPU_DISPATCH_REDUCED_FLOATING_TYPES_EXT(input_dtype, cos.scalar_type(), [&] {
scalar_t* q_ptr = query.data_ptr<scalar_t>();
scalar_t* k_ptr = key.data_ptr<scalar_t>();
const param_t* cos_ptr = cos.data_ptr<param_t>();
const param_t* sin_ptr = sin.data_ptr<param_t>();
for (int64_t d = 0; d < ndim; ++d) {
const int64_t offset = d * chunk_size;
auto cache_pos = [cos_ptr, sin_ptr, head_size, offset](int64_t token) -> SplitCosSinRow<param_t> {
return {cos_ptr + token * head_size + offset, sin_ptr + token * head_size + offset};
};
rotary_embedding_kernel_impl<scalar_t, RotaryMode::NeoxFull, true>(
q_ptr + offset, k_ptr + offset, q_ptr + offset, k_ptr + offset, p, cache_pos);
}
});
}
// 2D : [num_tokens, num_heads*head_size] inplace
// 3D : [num_tokens, num_heads, head_size] outplace
// 4D : [batch_size, seq_len, num_heads, head_size] inplace
@@ -376,7 +417,7 @@ apply_rotary_pos_emb_cpu(at::Tensor& query, at::Tensor& key, at::Tensor& cos, at
// key: [num_tokens, num_kv_heads * head_size]
// cos_sin_cache: [max_position_embeddings, rotary_dim]
// mrope_section: [t, h, w]
std::tuple<at::Tensor, at::Tensor> multimodal_rotary_embedding_cpu(
void multimodal_rotary_embedding_cpu(
at::Tensor& positions,
at::Tensor& query,
at::Tensor& key,
@@ -449,5 +490,4 @@ std::tuple<at::Tensor, at::Tensor> multimodal_rotary_embedding_cpu(
}
}
});
return std::make_tuple(query, key);
}
@@ -361,7 +361,8 @@ at::Tensor fused_experts_cpu(
const std::optional<at::Tensor>& w2_bias,
const std::optional<double>& alpha,
const std::optional<double>& limit,
bool is_vnni);
bool is_vnni,
const std::optional<std::string>& activation);
#if !defined(SGLANG_CPU_ARM64_SKIP_X86_ONLY_OPS)
at::Tensor shared_expert_cpu(
@@ -478,8 +479,11 @@ std::tuple<at::Tensor, at::Tensor> rotary_embedding_cpu(
std::tuple<at::Tensor, at::Tensor>
apply_rotary_pos_emb_cpu(at::Tensor& query, at::Tensor& key, at::Tensor& cos, at::Tensor& sin);
// multidimensional rope
void apply_multidimensional_rope_cpu(at::Tensor& query, at::Tensor& key, at::Tensor& cos, at::Tensor& sin);
// mrope
std::tuple<at::Tensor, at::Tensor> multimodal_rotary_embedding_cpu(
void multimodal_rotary_embedding_cpu(
at::Tensor& positions,
at::Tensor& query,
at::Tensor& key,
@@ -795,7 +799,7 @@ TORCH_LIBRARY_FRAGMENT(sgl_kernel, m) {
"fused_experts_cpu(Tensor hidden_states, Tensor w1, Tensor w2, Tensor topk_weights, Tensor topk_ids, bool "
"inplace, int moe_comp_method, Tensor? w1_scale, Tensor? w2_scale, "
"Tensor? w1_zero, Tensor? w2_zero, int[]? block_size, Tensor? w1_bias, Tensor? w2_bias, float? alpha, float? "
"limit, bool is_vnni) -> Tensor");
"limit, bool is_vnni, str? activation=None) -> Tensor");
m.impl("fused_experts_cpu", torch::kCPU, &fused_experts_cpu);
#if !defined(SGLANG_CPU_ARM64_SKIP_X86_ONLY_OPS)
@@ -864,10 +868,14 @@ TORCH_LIBRARY_FRAGMENT(sgl_kernel, m) {
m.def("apply_rotary_pos_emb_cpu(Tensor query, Tensor key, Tensor cos, Tensor sin) -> (Tensor, Tensor)");
m.impl("apply_rotary_pos_emb_cpu", torch::kCPU, &apply_rotary_pos_emb_cpu);
// multidimensional rope
m.def("apply_multidimensional_rope_cpu(Tensor(a!) query, Tensor(b!) key, Tensor cos, Tensor sin) -> ()");
m.impl("apply_multidimensional_rope_cpu", torch::kCPU, &apply_multidimensional_rope_cpu);
// multimodal rope
m.def(
"multimodal_rotary_embedding_cpu(Tensor positions, Tensor query, Tensor key, int head_size, Tensor "
"cos_sin_cache, int[]? mrope_section, bool mrope_interleaved, bool is_neox) -> (Tensor, Tensor)");
"multimodal_rotary_embedding_cpu(Tensor positions, Tensor(a!) query, Tensor(b!) key, int head_size, Tensor "
"cos_sin_cache, int[]? mrope_section, bool mrope_interleaved, bool is_neox) -> ()");
m.impl("multimodal_rotary_embedding_cpu", torch::kCPU, &multimodal_rotary_embedding_cpu);
// CPU and memory binding
+8
View File
@@ -612,6 +612,14 @@ inline at::vec::Vectorized<float> fast_silu(const at::vec::Vectorized<float>& x)
#endif
}
// exact (erf) gelu, matching torch.nn.functional.gelu(approximate="none")
inline at::vec::Vectorized<float> fast_gelu(const at::vec::Vectorized<float>& x) {
const auto half = at::vec::Vectorized<float>(0.5f);
const auto one = at::vec::Vectorized<float>(1.f);
const auto inv_sqrt2 = at::vec::Vectorized<float>(0.70710678118654752440f);
return half * x * (one + (x * inv_sqrt2).erf());
}
inline at::vec::Vectorized<float>
fast_sigmoid_glu(const at::vec::Vectorized<float>& x, const at::vec::Vectorized<float>& alpha) {
#if defined(CPU_CAPABILITY_AVX512)
+38 -4
View File
@@ -137,6 +137,32 @@ def adjust_tp_num_heads_if_necessary(model_config, tp_size, is_post_update):
)
def adjust_swa_num_heads_if_necessary(model_config, tp_size, weight_block_size):
# Sliding-window layers carry their own head counts, so the padded
# full-attention num_attention_heads does not describe them
from sglang.srt.layers.vocab_parallel_embedding import pad_vocab_size
text_config = model_config.hf_text_config
if not hasattr(text_config, "swa_num_key_value_heads"):
return
swa_num_key_value_heads = text_config.swa_num_key_value_heads
swa_num_attention_heads = getattr(
text_config, "swa_num_attention_heads", model_config.num_attention_heads
)
# ModelConfig always materializes swa_head_dim, defaulting it to head_dim.
swa_pad_size = get_num_heads_padding_size(
tp_size, weight_block_size, text_config.swa_head_dim
)
padded_num_key_value_heads = pad_vocab_size(swa_num_key_value_heads, swa_pad_size)
padded_num_attention_heads = padded_num_key_value_heads * (
swa_num_attention_heads // swa_num_key_value_heads
)
update_config(text_config, "swa_num_key_value_heads", padded_num_key_value_heads)
update_config(text_config, "swa_num_attention_heads", padded_num_attention_heads)
def update_intermediate_size(model_config, attr_name, intermediate_padding_size):
attr_value = intermediate_padding_size
if (
@@ -152,7 +178,7 @@ def update_intermediate_size(model_config, attr_name, intermediate_padding_size)
elif hasattr(model_config, attr_name):
attr_value = getattr(model_config, attr_name)
if attr_value % intermediate_padding_size != 0:
if attr_value is not None and attr_value % intermediate_padding_size != 0:
from sglang.srt.layers.vocab_parallel_embedding import pad_vocab_size
origin_value = attr_value
@@ -223,6 +249,10 @@ def adjust_config_with_unaligned_cpu_tp(
+ model_config.hf_config.qk_rope_head_dim,
)
# gate is on full-attention counts; Gemma 4 has 2 full-attention KV
# heads, so only TP 1 and 2 clear it and both align the 8 sliding heads
adjust_swa_num_heads_if_necessary(model_config, tp_size, weight_block_size)
query_heads_per_kv = (
model_config.num_attention_heads // model_config.get_total_num_kv_heads()
)
@@ -306,9 +336,13 @@ def adjust_config_with_unaligned_cpu_tp(
)
for m_config, config_name, model_type, num_head_str in multimodal_config:
if hasattr(m_config, config_name) and (
m_config.model_type == model_type
or getattr(m_config, config_name).model_type == model_type
if (
hasattr(m_config, config_name)
and getattr(m_config, config_name) is not None
and (
m_config.model_type == model_type
or getattr(m_config, config_name).model_type == model_type
)
):
num_heads = getattr(getattr(m_config, config_name), num_head_str)
@@ -59,6 +59,8 @@ class IntelAMXAttnBackend(AttentionBackend):
# sized [bs, num_head, num_kv_splits, v_head_dim + 1] to match.
self.num_kv_splits = 8
self._attn_logits_buffers: dict[tuple[int, int], torch.Tensor] = {}
# speculative decoding params
self.num_draft_tokens = get_spec().speculative_num_draft_tokens
@@ -216,6 +218,9 @@ class IntelAMXAttnBackend(AttentionBackend):
seq_lens = forward_batch.seq_lens
if seq_lens.dtype != torch.int64:
seq_lens = seq_lens.to(torch.int64)
# Gemma4's KV-shared layers pass k=v=None - the layer they share with
# already wrote their extend K/V to the cache
self.extend_attention_fwd(
q.view(-1, layer.tp_q_head_num, layer.qk_head_dim),
k,
@@ -237,7 +242,7 @@ class IntelAMXAttnBackend(AttentionBackend):
sinks,
tree_mask,
)
return o
return o.view(-1, layer.tp_q_head_num * layer.v_head_dim)
def forward_decode(
self,
@@ -249,8 +254,6 @@ class IntelAMXAttnBackend(AttentionBackend):
save_kv_cache=True,
sinks=None,
):
attn_logits, _ = self.forward_metadata
if self.draft_decode_metadata is not None:
req_to_token, seq_lens, req_pool_indices = self.draft_decode_metadata
else:
@@ -263,6 +266,15 @@ class IntelAMXAttnBackend(AttentionBackend):
if seq_lens.dtype != torch.int64:
seq_lens = seq_lens.to(torch.int64)
if layer.v_head_dim == self.v_head_dim and layer.tp_q_head_num == self.num_head:
attn_logits, _ = self.forward_metadata
else:
# This layer's shape differs from the model-wide metadata buffer -
# size from the same seq_lens the kernel derives num_seqs from
attn_logits = self._get_attn_logits_buffer(
seq_lens.shape[0], layer.tp_q_head_num, layer.v_head_dim
)
if layer.qk_head_dim != layer.v_head_dim:
o = q.new_empty((q.shape[0], layer.tp_q_head_num * layer.v_head_dim))
else:
@@ -291,7 +303,23 @@ class IntelAMXAttnBackend(AttentionBackend):
forward_batch.encoder_lens,
sinks,
)
return o
return o.view(-1, layer.tp_q_head_num * layer.v_head_dim)
def _get_attn_logits_buffer(
self, num_seqs: int, num_heads: int, v_head_dim: int
) -> torch.Tensor:
key = (num_heads, v_head_dim)
buffer = self._attn_logits_buffers.get(key)
if buffer is None or buffer.shape[0] < num_seqs:
# decode_attention_cpu writes every element it later reads, so the
# buffer needs no initialization and can be reused; it only grows.
buffer = torch.empty(
(num_seqs, num_heads, self.num_kv_splits, v_head_dim + 1),
dtype=torch.float32,
device=self.device,
)
self._attn_logits_buffers[key] = buffer
return buffer[:num_seqs]
def support_triton(self):
return False
+4
View File
@@ -1310,6 +1310,10 @@ class Gemma4RMSNorm(BaseFusedOp):
def forward_cpu(self, x: torch.Tensor) -> torch.Tensor:
if _is_cpu_amx_available:
# the kernel needs a last-dim-contiguous input; the audio conformer
# normalizes its depthwise conv output, which arrives permuted
if x.stride(-1) != 1:
x = x.contiguous()
return torch.ops.sgl_kernel.gemma4_rmsnorm_cpu(
x, self.weight.data, self.eps, self.scale_shift, self.with_scale
)
@@ -2406,6 +2406,7 @@ class Fp8MoEMethod(FusedMoEMethodBase):
None, # alpha
None, # limit
True, # is_vnni
moe_runner_config.activation, # activation
)
return StandardCombineInput(hidden_states=output)
@@ -1417,6 +1417,7 @@ class Mxfp4MoEMethod(FusedMoEMethodBase):
layer.moe_runner_config.gemm1_alpha,
layer.moe_runner_config.gemm1_clamp_limit,
True, # is_vnni
layer.moe_runner_config.activation, # activation
)
else:
from sglang.srt.layers.moe.fused_moe_native import moe_forward_native
@@ -849,10 +849,6 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, BaseFusedOp):
moe_runner_config = self.moe_runner_config
assert (
moe_runner_config.activation == "silu"
), f"activation = {moe_runner_config.activation} is not supported."
if use_intel_amx_backend(layer):
from sglang.srt.layers.moe.topk import apply_topk_weights_cpu
@@ -878,6 +874,7 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, BaseFusedOp):
layer.moe_runner_config.gemm1_alpha,
layer.moe_runner_config.gemm1_clamp_limit,
True, # is_vnni
moe_runner_config.activation, # activation
)
return StandardCombineInput(hidden_states=output)
else:
@@ -380,6 +380,7 @@ class W8A8Int8MoEMethod(FusedMoEMethodBase):
None, # alpha
None, # limit
True, # is_vnni
self.moe_runner_config.activation, # activation
)
return StandardCombineInput(hidden_states=output)
@@ -224,7 +224,7 @@ class MRotaryEmbedding(RotaryEmbedding):
fused_set_kv_buffer_arg=None,
) -> Tuple[torch.Tensor, torch.Tensor]:
if _is_cpu_amx_available:
return torch.ops.sgl_kernel.multimodal_rotary_embedding_cpu(
torch.ops.sgl_kernel.multimodal_rotary_embedding_cpu(
positions,
query,
key,
@@ -234,6 +234,7 @@ class MRotaryEmbedding(RotaryEmbedding):
self.mrope_interleaved,
self.is_neox_style,
)
return query, key
return self.forward_native(positions, query, key, fused_set_kv_buffer_arg)
def forward_cuda(
@@ -176,6 +176,8 @@ def register_fake_ops(tp_size: int):
"gemma_fused_add_rmsnorm_cpu",
"layernorm_cpu",
"fused_add_layernorm_cpu",
"multimodal_rotary_embedding_cpu",
"apply_multidimensional_rope_cpu",
]
for op in none_return_ops:
@@ -259,28 +261,14 @@ def register_fake_ops(tp_size: int):
@register_cpu_compile_fake("rotary_embedding_cpu")
def _(positions, query, key, head_size, cos_sin_cache, is_neox):
if query.ndim == 2:
return query, key
else:
return torch.empty_like(query), torch.empty_like(key)
# TODO: the kernel aliases query/key for 2D and 4D but allocates for 3D,
# which no schema expresses; an accurate fake needs it to pick one
return torch.empty_like(query), torch.empty_like(key)
@register_cpu_compile_fake("apply_rotary_pos_emb_cpu")
def _(query, key, cos, sin):
return query, key
@register_cpu_compile_fake("multimodal_rotary_embedding_cpu")
def _(
positions,
query,
key,
head_size,
cos_sin_cache,
mrope_section,
mrope_interleaved,
is_neox,
):
return query, key
@register_cpu_compile_fake("qkv_proj_with_rope_fused_weight")
def _(
hidden_states,
+27 -9
View File
@@ -74,6 +74,21 @@ Gemma4MLP = Gemma3MLP
Gemma4TextScaledWordEmbedding = Gemma3TextScaledWordEmbedding
def load_tied_lm_head(
loaded_weight, *, params_dict, loaded_params, head_param_name="lm_head.weight"
):
"""Load a tied embedding into an lm_head the runtime could not alias.
No-op when this rank holds no lm_head.
"""
head_param = params_dict.get(head_param_name)
if head_param is None:
return
wl = getattr(head_param, "weight_loader", default_weight_loader)
wl(head_param, loaded_weight)
loaded_params.add(head_param_name)
def pp_filter_load_weight(
name,
loaded_weight,
@@ -109,11 +124,12 @@ def pp_filter_load_weight(
return True
if tie_word_embeddings and pp_group.is_last_rank and name == embed_weight_name:
head_param = params_dict.get(head_param_name)
if head_param is not None:
wl = getattr(head_param, "weight_loader", default_weight_loader)
wl(head_param, loaded_weight)
loaded_params.add(head_param_name)
load_tied_lm_head(
loaded_weight,
params_dict=params_dict,
loaded_params=loaded_params,
head_param_name=head_param_name,
)
return True
if not pp_group.is_first_rank and any(p in name for p in first_rank_only_patterns):
@@ -297,16 +313,18 @@ class Gemma4Attention(nn.Module):
else -1
)
self.total_num_heads = config.num_attention_heads
assert self.total_num_heads % tp_size == 0
self.num_heads = self.total_num_heads // tp_size
if layer_type == "sliding_attention":
self.total_num_heads = getattr(
config, "swa_num_attention_heads", config.num_attention_heads
)
self.total_num_kv_heads = getattr(
config, "swa_num_key_value_heads", config.num_key_value_heads
)
else:
self.total_num_heads = config.num_attention_heads
self.total_num_kv_heads = config.num_key_value_heads
assert self.total_num_heads % tp_size == 0
self.num_heads = self.total_num_heads // tp_size
self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
+29 -11
View File
@@ -59,13 +59,20 @@ from sglang.srt.model_loader.weight_utils import (
maybe_remap_kv_scale_name,
)
from sglang.srt.models.gemma4_audio import Gemma4AudioEncoder
from sglang.srt.models.gemma4_causal import Gemma4TextModel, pp_filter_load_weight
from sglang.srt.models.gemma4_causal import (
Gemma4TextModel,
load_tied_lm_head,
pp_filter_load_weight,
)
from sglang.srt.models.gemma4_vision import Gemma4VisionEncoder
from sglang.srt.utils import add_prefix
from sglang.srt.utils import add_prefix, cpu_has_amx_support, is_cpu
from sglang.srt.utils.hf_transformers_utils import get_processor
logger = logging.getLogger(__name__)
_is_cpu_amx_available = cpu_has_amx_support()
_is_cpu = is_cpu()
cached_get_processor = lru_cache(get_processor)
@@ -241,9 +248,15 @@ class Gemma4ForConditionalGeneration(PreTrainedModel):
# while logits run on the last rank, so we can't reuse the embedding
# module directly. For PP=1 keep the original tying; for PP>1
# materialize a real ParallelLMHead on the last rank and route the
# checkpoint embedding into it during load_weights.
# checkpoint embedding into it during load_weights. CPU with AMX does
# the same: the packed head weights cannot alias the embedding table.
text_tie = getattr(text_config, "tie_word_embeddings", True)
if self.pp_group.world_size == 1 and text_tie:
self.lm_head_is_tied = (
self.pp_group.world_size == 1
and text_tie
and not (_is_cpu and _is_cpu_amx_available)
)
if self.lm_head_is_tied:
self.lm_head = self.language_model.embed_tokens
elif self.pp_group.is_last_rank:
self.lm_head = ParallelLMHead(
@@ -668,13 +681,8 @@ class Gemma4ForConditionalGeneration(PreTrainedModel):
if self.capture_aux_hidden_states:
hidden_states, aux_hidden_states = hidden_states
# PP=1 keeps the original tied-weight behavior of using embed_tokens
# directly; under PP we route through the dedicated lm_head module.
head = (
self.language_model.embed_tokens
if self.pp_group.world_size == 1
and getattr(self.config.text_config, "tie_word_embeddings", True)
else self.lm_head
self.language_model.embed_tokens if self.lm_head_is_tied else self.lm_head
)
return self.logits_processor(
input_ids,
@@ -1016,6 +1024,16 @@ class Gemma4ForConditionalGeneration(PreTrainedModel):
param, "weight_loader", default_weight_loader
)
weight_loader(param, loaded_weight)
if (
text_tie
and not self.lm_head_is_tied
and name == "language_model.embed_tokens.weight"
):
load_tied_lm_head(
loaded_weight,
params_dict=params_dict,
loaded_params=loaded_params,
)
loaded_params.add(name)
unloaded_params = params_dict.keys() - loaded_params
if unloaded_params:
@@ -1095,7 +1113,7 @@ class Gemma4ForConditionalGeneration(PreTrainedModel):
"--pp-size 1 if you need this API."
)
embed = self.language_model.embed_tokens.weight
# Gemma4 ties word embeddings, so embed_tokens serves as lm_head
# a materialized lm_head is loaded from this very tensor, so it is exact
return embed, embed
def set_eagle3_layers_to_capture(self, layer_ids: Optional[List[int]] = None):
+25 -6
View File
@@ -30,7 +30,17 @@ from sglang.srt.layers.clippable_linear import (
from sglang.srt.layers.layernorm import Gemma4RMSNorm
from sglang.srt.layers.quantization.base_config import QuantizationConfig
from sglang.srt.runtime_context import get_mm, get_parallel
from sglang.srt.utils import add_prefix, get_device_capability, is_cuda, is_hip
from sglang.srt.utils import (
add_prefix,
cpu_has_amx_support,
get_device_capability,
is_cpu,
is_cuda,
is_hip,
)
_is_cpu = is_cpu()
_is_cpu_amx_available = _is_cpu and cpu_has_amx_support()
# ---------------------------------------------------------------------------
# 2-D Multidimensional RoPE (matches HF Gemma4RotaryEmbedding for vision)
@@ -173,7 +183,9 @@ class Gemma4VisionAttention(nn.Module):
num_heads=self.num_heads_per_partition,
num_kv_heads=self.num_kv_heads_per_partition,
dropout=0.0,
flatten_batch=True,
# sdpa asserts bsz == 1 under flatten_batch, which batched video
# frames violate; Gemma 4 passes its own 4-D mask regardless
flatten_batch=backend != "sdpa",
softmax_in_single_precision=False,
softmax_scale=1.0,
)
@@ -198,6 +210,8 @@ class Gemma4VisionAttention(nn.Module):
# ROCm: use triton_attn to avoid SDPA flatten_batch issues
# with multi-image/video inputs
return "triton_attn"
# not amx_attn: VisionAMXAttention swallows softmax_scale and the mask
# in **kwargs, and the CPU flash_attn hardcodes sm_scale
return "sdpa"
def forward(
@@ -219,10 +233,15 @@ class Gemma4VisionAttention(nn.Module):
k = self.k_norm(k.reshape(-1, self.head_dim)).reshape(k.shape)
v = self.v_norm(v.reshape(-1, self.head_dim)).reshape(v.shape)
cos_flat = cos.reshape(bsz * seq_len, 1, self.head_dim)
sin_flat = sin.reshape(bsz * seq_len, 1, self.head_dim)
q = _apply_multidimensional_rope(q, cos_flat, sin_flat)
k = _apply_multidimensional_rope(k, cos_flat, sin_flat)
if _is_cpu_amx_available:
cos = cos.reshape(bsz * seq_len, self.head_dim)
sin = sin.reshape(bsz * seq_len, self.head_dim)
torch.ops.sgl_kernel.apply_multidimensional_rope_cpu(q, k, cos, sin)
else:
cos_flat = cos.reshape(bsz * seq_len, 1, self.head_dim)
sin_flat = sin.reshape(bsz * seq_len, 1, self.head_dim)
q = _apply_multidimensional_rope(q, cos_flat, sin_flat)
k = _apply_multidimensional_rope(k, cos_flat, sin_flat)
if attention_mask is not None:
attn_mask_4d = (
+9 -3
View File
@@ -5635,13 +5635,19 @@ class ServerArgs:
# Default attention backend selection moved to the override registry
# (arg_groups/overrides.py: _gemma4_overrides).
prefill_backend, decode_backend = self._resolved_attention_backends()
accepted_backends = ("trtllm_mha", "triton", "ascend", "intel_xpu")
accepted_backends = (
"trtllm_mha",
"triton",
"ascend",
"intel_xpu",
"intel_amx",
)
assert (
prefill_backend in accepted_backends
and decode_backend in accepted_backends
), (
"Gemma4 only supports trtllm_mha, triton, or intel_xpu attention backend, "
f"got prefill={prefill_backend}, decode={decode_backend}"
"Gemma4 only supports trtllm_mha, triton, ascend, intel_xpu, or intel_amx "
f"attention backend, got prefill={prefill_backend}, decode={decode_backend}"
)
# The quantization/moe_runner_backend resolution moved to the override
+19 -6
View File
@@ -1,3 +1,4 @@
import functools
import itertools
import math
@@ -198,7 +199,17 @@ def scaled_weight(weight, scales):
return weight_scaled
def torch_naive_fused_moe(a, w1, w2, score, topk, renormalize):
def _activation_fn(activation):
"""Reference gate-and-multiply for the activation fused_experts_cpu applies."""
if activation == "silu":
return SiluAndMul
if activation == "gelu":
# matches the erf gelu the kernel computes
return functools.partial(GeluAndMul, approximate="none")
raise ValueError(f"Unsupported activation: {activation}")
def torch_naive_fused_moe(a, w1, w2, score, topk, renormalize, activation="silu"):
B, D = a.shape
a = a.view(B, -1, D).repeat(1, topk, 1).reshape(-1, D)
out = torch.zeros(B * topk, w2.shape[1], dtype=a.dtype, device=a.device)
@@ -208,14 +219,14 @@ def torch_naive_fused_moe(a, w1, w2, score, topk, renormalize):
if renormalize:
topk_weight = topk_weight / topk_weight.sum(dim=-1, keepdim=True)
act_fn = _activation_fn(activation)
topk_weight = topk_weight.view(-1)
topk_ids = topk_ids.view(-1)
for i in range(w1.shape[0]):
mask = topk_ids == i
if mask.sum():
out[mask] = SiluAndMul(a[mask] @ w1[i].transpose(0, 1)) @ w2[i].transpose(
0, 1
)
out[mask] = act_fn(a[mask] @ w1[i].transpose(0, 1)) @ w2[i].transpose(0, 1)
return (
out.view(B, -1, w2.shape[1]) * topk_weight.view(B, -1, 1).to(out.dtype)
).sum(dim=1)
@@ -372,11 +383,13 @@ def torch_w8a8_per_column_fused_moe(a, w1, w2, w1_s, w2_s, topk_weight, topk_ids
)
def native_fp8_fused_moe(a, w1, w2, topk_weight, topk_ids, topk):
def native_fp8_fused_moe(a, w1, w2, topk_weight, topk_ids, topk, activation="silu"):
B, D = a.shape
a = a.view(B, -1, D).repeat(1, topk, 1).reshape(-1, D).float()
out = torch.zeros(B * topk, w2.shape[1], dtype=torch.float32, device=a.device)
act_fn = _activation_fn(activation)
# Calculate routing
topk_weight = topk_weight.view(-1)
topk_ids = topk_ids.view(-1)
@@ -385,7 +398,7 @@ def native_fp8_fused_moe(a, w1, w2, topk_weight, topk_ids, topk):
mask = topk_ids == i
if mask.sum():
ic0 = torch.matmul(a[mask], w1[i].transpose(0, 1))
ic1 = SiluAndMul(ic0)
ic1 = act_fn(ic0)
out[mask] = torch.matmul(ic1, w2[i].transpose(0, 1))
return (