Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Libraries/MLXLLM/Models/Gemma4.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public struct Gemma4Configuration: Codable, Sendable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let quantizationContainer = try decoder.container(keyedBy: QuantizationCodingKeys.self)
let rootPerLayerQuantization = (try? BaseConfiguration(from: decoder))?.perLayerQuantization
self.modelType = try container.decodeIfPresent(String.self, forKey: .modelType) ?? "gemma4"
self.vocabSize = try container.decodeIfPresent(Int.self, forKey: .vocabSize) ?? 262144

Expand All @@ -52,9 +53,12 @@ public struct Gemma4Configuration: Codable, Sendable {
Gemma4WeightQuantizationMetadata.self, forKey: .quantization)
?? quantizationContainer.decodeIfPresent(
Gemma4WeightQuantizationMetadata.self, forKey: .quantizationConfig)
if let quantization {
if let rootPerLayerQuantization {
self.textConfig.mergeQuantization(rootPerLayerQuantization)
} else if let quantization {
self.textConfig.quantizationBits = quantization.bits
self.textConfig.quantizationGroupSize = quantization.groupSize
self.textConfig.quantizationMode = quantization.mode ?? .affine
}
}
}
Expand Down
30 changes: 14 additions & 16 deletions Libraries/MLXLLM/Models/Gemma4MTPConfigurationValidation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,10 @@ enum Gemma4AssistantConfigurationValidator {
"textConfig.numGlobalKeyValueHeads",
"is required when full attention uses K=V")
}
let fullKVHeads = text.attentionKeqV
? text.numGlobalKeyValueHeads!
: text.numKeyValueHeads
// Mirrors Gemma4Attention.init: full layers honor
// num_global_key_value_heads whenever present, independent of
// attention_k_eq_v (k_eq_v only elides v_proj).
let fullKVHeads = text.numGlobalKeyValueHeads ?? text.numKeyValueHeads
try divides(
fullKVHeads,
into: text.numAttentionHeads,
Expand Down Expand Up @@ -266,9 +267,9 @@ enum Gemma4AssistantConfigurationValidator {
field: "textConfig.slidingAttention")
}
if hasFull {
let fullKVHeads = text.attentionKeqV
? text.numGlobalKeyValueHeads!
: text.numKeyValueHeads
// Full layers honor num_global_key_value_heads whenever present,
// independent of attention_k_eq_v (mirrors Gemma4Attention.init).
let fullKVHeads = text.numGlobalKeyValueHeads ?? text.numKeyValueHeads
try validateAttentionProducts(
heads: text.numAttentionHeads,
kvHeads: fullKVHeads,
Expand Down Expand Up @@ -584,12 +585,9 @@ enum Gemma4MTPCompatibilityValidator {
drafterText.attentionKeqV,
target.attentionKeqV,
field: "fullAttention.attentionKeqV")
if drafterText.attentionKeqV {
try equalOptional(
drafterText.numGlobalKeyValueHeads,
target.numGlobalKeyValueHeads,
field: "fullAttention.numGlobalKeyValueHeads")
}
// The gated `numGlobalKeyValueHeads` equality check was
// removed with the k_eq_v-gated head rule: full-layer KV
// geometry is now compared unconditionally right below.
try equal(
effectiveFullKVHeads(drafterText),
effectiveFullKVHeads(target),
Expand All @@ -611,10 +609,10 @@ enum Gemma4MTPCompatibilityValidator {
}

private static func effectiveFullKVHeads(_ config: Gemma4TextConfiguration) -> Int {
if config.attentionKeqV, let global = config.numGlobalKeyValueHeads {
return global
}
return config.numKeyValueHeads
// Mirrors Gemma4Attention.init: full layers honor
// num_global_key_value_heads whenever present, independent of
// attention_k_eq_v (k_eq_v only elides v_proj).
config.numGlobalKeyValueHeads ?? config.numKeyValueHeads
}

private static func equal<T: Equatable & CustomStringConvertible>(
Expand Down
14 changes: 5 additions & 9 deletions Libraries/MLXLLM/Models/Gemma4MTPTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,10 @@ import MLXLMCommon
/// Abstraction over a Gemma 4 text tower that can drive MTP speculative
/// decoding.
///
/// Both the text-only ``Gemma4TextModel`` (MLXLLM) and the vision-language
/// `Gemma4` tower (MLXVLM) conform, so the single-stream MTP round loop, the
/// token iterator, and the drafter binding all work against either tower. The
/// MTP drafter (``Gemma4AssistantDraftModel``) is trained against the Gemma 4
/// text architecture; because the VLM tower implements the *same* text
/// architecture and loads the *same* text weights, a drafter bound to a VLM
/// tower produces the same speculative tokens it would against the text-only
/// tower (validated by the parity spike).
/// ``Gemma4TextModel`` is the canonical target for text-only loads and for
/// MLXVLM Gemma 4: the VLM owns and exposes this exact object as `textModel`.
/// Single-stream and CBv2 MTP therefore bind to the same text architecture,
/// weights, hidden capture, and cache identity used by direct VLM forwards.
public protocol Gemma4MTPTarget: AnyObject {

/// The resolved text configuration, used for drafter-compatibility
Expand All @@ -40,7 +36,7 @@ public protocol Gemma4MTPTarget: AnyObject {
_ caches: [KVCache], accepted: Gemma4AcceptCount, blockSize: Int)
}

// MARK: - Text-only tower conformance
// MARK: - Shared text-tower conformance

extension Gemma4TextModel: Gemma4MTPTarget {
public var mtpConfiguration: Gemma4TextConfiguration { configuration }
Expand Down
Loading