-
Notifications
You must be signed in to change notification settings - Fork 113
perf(qwen36): qualify M4 head-256 attention #640
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| # Qwen3.6 Head-Dimension-256 Attention Qualification | ||
|
|
||
| Date: 2026-08-17 | ||
|
|
||
| Hardware: Apple M4 Max, 40 GPU cores, 128 GB unified memory | ||
|
|
||
| Artifact: `EigenLabs/Qwen3.6-35B-A3B-MLX-VL-4bit-g64-router8-mtp` | ||
|
|
||
| Qualified dependency identities: | ||
|
|
||
| | Repository | Commit | | ||
| |---|---| | ||
| | `Layr-Labs/mlx` | `4937e294fccb2513b2e3e57d1d1c25cc646001c3` | | ||
| | `Layr-Labs/mlx-c` | `1dd06c957e3ef694de8bab9c3bdc687e6a75ff0f` | | ||
| | `Layr-Labs/mlx-swift` | `c9a6c77b3aff2ba1b0fe545e72c65105c55e8c28` | | ||
| | `Layr-Labs/mlx-swift-lm` | `5307541a915022df4084131f82e2ac49a3cf9faf` | | ||
|
|
||
| Source-matched no-JIT metallib SHA-256: | ||
| `bbbdecbdacb2406f0fde6454493d4a9de82685047c69531977d90e3829c88ec7`. | ||
|
|
||
| ## Result | ||
|
|
||
| The existing MLX D256 Steel template is a valid bounded-memory path, not a | ||
| speed path on M4 Max. The release therefore separates the two decisions: | ||
|
|
||
| - qualified M4 Max providers use composed attention with qL=512 for speed; | ||
| - `force_fused` explicitly requests the D256 Steel kernel when bounded | ||
| transient memory is more important than throughput; | ||
| - automatic fused selection remains unqualified and fails closed; | ||
| - other hardware retains the historical qL=128 posture pending measurement. | ||
|
|
||
| ## Production A/B | ||
|
|
||
| Release builds, production CBv2 engine, contiguous KV, source-matched no-JIT | ||
| metallib. Medians of two same-build iterations: | ||
|
|
||
| | Prompt | qL=128 control | qualified qL=512 | Improvement | | ||
| |---:|---:|---:|---:| | ||
| | 8k | 6,206.8 ms | 6,056.6 ms | 2.4% | | ||
| | 32k | 32,879.5 ms | 31,660.9 ms | 3.7% (1.22 s) | | ||
|
|
||
| Five-iteration confirmation measured 6,296.9 -> 6,206.5 ms at 8k and | ||
| 33,012.7 -> 31,693.8 ms at 32k. The qL=512 result uses 40.6 MiB more | ||
| transient MLX memory at 32k (1.974 GB vs 1.931 GB, +2.2%). | ||
|
|
||
| Production measurement command (control uses an explicit block of 128; the | ||
| qualified arm omits the override): | ||
|
|
||
| ```sh | ||
| ./.build/release/darkbloom benchmark \ | ||
| --model EigenLabs/Qwen3.6-35B-A3B-MLX-VL-4bit-g64-router8-mtp \ | ||
| --scheduler-prefill --prefill-lengths 8192,32768 \ | ||
| --prefill-iterations 5 --kv-backend contiguous | ||
| ``` | ||
|
|
||
| The explicit forced-fused arm measured 33,063.0 ms and 1.668 GB transient at | ||
| 32k: 15.5% less transient memory than qualified qL=512, but slower. It is not | ||
| an automatic performance route. | ||
|
|
||
| ## Kernel Experiments | ||
|
|
||
| Four separate fused designs were implemented, correctness-tested, profiled, | ||
| and removed after losing the composed-path gate: | ||
|
|
||
| 1. Q8, K32/K64/K128, four SIMD groups, D64 output shards. | ||
| 2. Q32 with D128 shards, eight SIMD groups. | ||
| 3. Q16 across two query heads sharing one KV head. | ||
| 4. Q8 across all eight GQA query heads, 512-thread workgroup. | ||
|
|
||
| All passed lower-right causal, GQA8, non-contiguous BHLD, tail, and NaN gates. | ||
| The best fused experiment still lost composed attention by more than 2x at | ||
| long context. No experimental kernel or selector remains in the source tree. | ||
|
|
||
| ## Controls | ||
|
|
||
| - `DARKBLOOM_CBV2_ATTN_EXECUTION=fallback`: composed attention. | ||
| - `DARKBLOOM_CBV2_ATTN_EXECUTION=fused`: explicit D256 Steel path. | ||
| - `DARKBLOOM_CBV2_ATTN_EXECUTION=auto`: fails closed until hardware-specific | ||
| fused qualification exists. | ||
| - `DARKBLOOM_CBV2_ATTN_QUERY_BLOCK=<n>`: operator override; `0` disables | ||
| composed query blocking. | ||
|
|
||
| The default qL=512 qualification is intentionally limited to the exact tested | ||
| `Mac16,5` configuration: Qwen3.6, M4 Max, 40 GPU cores, and 128 GB RAM. M3 Max | ||
| qualification is a separate hardware run; M5/NAX is deferred to the next | ||
| workstream. | ||
|
|
||
| ## Release Gates | ||
|
|
||
| - MLX D256 FP16/BF16 masks, tails, sinks, long-K barriers, CPU rejection, | ||
| source compatibility, VJP, and forced-vmap rejection. | ||
| - mlx-c legacy and v2 ABI link tests. | ||
| - mlx-swift full test suite and generated-source regeneration. | ||
| - mlx-swift-lm cache-route, query-block, MTP/decode/span, and policy tests. | ||
| - Source-matched no-JIT metallib symbol verification before and after signing. | ||
| - Real-artifact default and forced-fused Qwen production canaries. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,14 +11,18 @@ public struct SchedulerPrefillBenchmarkReport: Codable, Sendable { | |
| /// UNVERSIONED payload predates the backend pin and cannot say which | ||
| /// backend it measured, so a gate must refuse it rather than assume. | ||
| /// 2 adds required effective config-projected Gemma settings. | ||
| public static let currentSchemaVersion = 2 | ||
| /// 3 adds per-sample MLX active, peak, and transient memory evidence. | ||
| public static let currentSchemaVersion = 3 | ||
|
|
||
| public struct Sample: Codable, Sendable { | ||
| public let strategy: String | ||
| public let promptTokens: Int | ||
| public let iteration: Int | ||
| public let ttftMs: Double | ||
| public let msPerPrefillToken: Double | ||
| public let activeMemoryBeforeBytes: Int | ||
| public let peakMemoryBytes: Int | ||
| public let transientPeakBytes: Int | ||
|
Comment on lines
+23
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this benchmark is used for the qL=128 versus qL=512 or composed versus fused comparisons introduced here, the JSON records memory and the resolved KV backend but not the requested or resolved attention control/query-block size. Because those arms are selected through process environment, a missing, malformed, or unforwarded variable can make both runs execute the same arm while the strict Python validator still accepts them as a valid comparison. Serialize the effective attention policy and block size in each sample or report and require the expected values during validation. Useful? React with 👍 / 👎. |
||
| /// The backend THIS sample's engine actually resolved to. Per sample | ||
| /// rather than once per run because each measurement builds its own | ||
| /// engine: a selection can be honoured at L=128 and degrade at | ||
|
|
@@ -135,7 +139,11 @@ public enum SchedulerPrefillBenchmark { | |
| resolved.append(sample.resolvedKVBackend) | ||
| log(" engine resolved kv backend: \(sample.resolvedKVBackend)") | ||
| } | ||
| log(" \(strategyLabel) L=\(length) i=\(iteration): \(String(format: "%.3f", sample.msPerPrefillToken)) ms/t (\(String(format: "%.1f", sample.ttftMs)) ms)") | ||
| log( | ||
| " \(strategyLabel) L=\(length) i=\(iteration): " | ||
| + "\(String(format: "%.3f", sample.msPerPrefillToken)) ms/t " | ||
| + "(\(String(format: "%.1f", sample.ttftMs)) ms), " | ||
| + "transient peak \(sample.transientPeakBytes) B") | ||
| samples.append(sample) | ||
| } | ||
| } | ||
|
|
@@ -198,6 +206,9 @@ public enum SchedulerPrefillBenchmark { | |
| let engine = parts.engine | ||
|
|
||
| let prompt = ThroughputSweep.tile(baseTokens, to: promptTokens, offset: iteration * 17) | ||
| Stream().synchronize() | ||
| let activeMemoryBefore = Memory.activeMemory | ||
| GPU.resetPeakMemory() | ||
| let started = ContinuousClock.now | ||
| let stream = try engine.submit(CBv2Request( | ||
| id: CBv2RequestID(1), | ||
|
|
@@ -222,13 +233,18 @@ public enum SchedulerPrefillBenchmark { | |
| let elapsed = firstOutput ?? (ContinuousClock.now - started) | ||
| let ttftMs = ThroughputSweep.seconds(elapsed) * 1000.0 | ||
| let prefillTokens = max(1, promptTokens - 1) | ||
| Stream().synchronize() | ||
| let peakMemory = Memory.peakMemory | ||
| await stopAndReclaim(engine) | ||
| return SchedulerPrefillBenchmarkReport.Sample( | ||
| strategy: strategyLabel, | ||
| promptTokens: promptTokens, | ||
| iteration: iteration, | ||
| ttftMs: ttftMs, | ||
| msPerPrefillToken: ttftMs / Double(prefillTokens), | ||
| activeMemoryBeforeBytes: activeMemoryBefore, | ||
| peakMemoryBytes: peakMemory, | ||
| transientPeakBytes: max(0, peakMemory - activeMemoryBefore), | ||
| resolvedKVBackend: parts.resolvedBackend | ||
| ) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -500,6 +500,7 @@ extension EngineV2Factory { | |
| kvBackend: EngineV2KVBackendSelection = .auto, | ||
| maxContextLength: Int? = nil, | ||
| environment: [String: String] = ProcessInfo.processInfo.environment, | ||
| attentionExecutionPolicy: CBv2AttentionExecutionPolicy? = nil, | ||
| pagedPreflightOverride: (([CBv2LayerKind]) throws -> Void)? = nil | ||
| ) throws -> ProductionBuild { | ||
| let preparedBackend = try prepareProductionBackend( | ||
|
|
@@ -509,6 +510,7 @@ extension EngineV2Factory { | |
| kvBackend: kvBackend, | ||
| maxContextLength: maxContextLength, | ||
| environment: environment, | ||
| attentionExecutionPolicy: attentionExecutionPolicy, | ||
| pagedPreflightOverride: pagedPreflightOverride) | ||
| return try assembleProductionBuild( | ||
| model: model, | ||
|
|
@@ -530,6 +532,7 @@ extension EngineV2Factory { | |
| kvBackend: EngineV2KVBackendSelection = .auto, | ||
| maxContextLength: Int? = nil, | ||
| environment: [String: String] = ProcessInfo.processInfo.environment, | ||
| attentionExecutionPolicy: CBv2AttentionExecutionPolicy? = nil, | ||
| pagedPreflightOverride: (([CBv2LayerKind]) throws -> Void)? = nil | ||
| ) throws -> ProductionBackendPreparation { | ||
| guard kvBytesCapacity > 0 else { | ||
|
|
@@ -554,19 +557,23 @@ extension EngineV2Factory { | |
| // at build time — never on the step path). | ||
| let layerKinds: [CBv2LayerKind] | ||
| let modelCapabilities: CBv2ModelCapabilities | ||
| let isQwen35: Bool | ||
| let newCaches: | ||
| ((Int, CBv2LayerKind) -> any CBv2AttendingLayerCache) | ||
| throws -> [any CBv2AttendingLayerCache] | ||
| switch model { | ||
| case let gemma as Gemma4TextModel: | ||
| isQwen35 = false | ||
| layerKinds = gemma.cbv2LayerKinds | ||
| modelCapabilities = .attentionOnly | ||
| newCaches = { make in try gemma.newCacheV2(makeLayerCache: make) } | ||
| case let gptoss as GPTOSSModel: | ||
| isQwen35 = false | ||
| layerKinds = gptoss.cbv2LayerKinds | ||
| modelCapabilities = .attentionOnly | ||
| newCaches = { make in gptoss.newCacheV2(makeLayerCache: make) } | ||
| case let qwen as Qwen35MoEModel: | ||
| isQwen35 = true | ||
| layerKinds = qwen.cbv2LayerKinds | ||
| modelCapabilities = qwen.cbv2Capabilities | ||
| newCaches = { make in qwen.newCacheV2(makeLayerCache: make) } | ||
|
|
@@ -575,6 +582,11 @@ extension EngineV2Factory { | |
| String(describing: type(of: model))) | ||
| } | ||
|
|
||
| let resolvedAttentionExecutionPolicy = attentionExecutionPolicy | ||
| ?? QwenHead256AttentionTuning.currentMachinePolicy( | ||
| isQwen35: isQwen35, | ||
|
Comment on lines
582
to
+587
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On the qualified machine, every Useful? React with 👍 / 👎. |
||
| environment: environment) | ||
|
|
||
| var resolvedKind: EngineV2KVBackendKind | ||
| switch kvBackend { | ||
| case .contiguous: resolvedKind = .contiguous | ||
|
|
@@ -787,7 +799,10 @@ extension EngineV2Factory { | |
| let backend = CBv2ContiguousKVBackend( | ||
| config: CBv2ContiguousBackendConfig(bytesCapacity: cappedCapacity)) | ||
| let caches = try newCaches { index, kind in | ||
| CBv2LayerCache(layerIndex: index, kind: kind) | ||
| CBv2LayerCache( | ||
| layerIndex: index, | ||
| kind: kind, | ||
| attentionExecutionPolicy: resolvedAttentionExecutionPolicy) | ||
| } | ||
| return ProductionBackendPreparation( | ||
| model: model, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import Foundation | ||
| import MLXLMCommon | ||
|
|
||
| enum QwenHead256AttentionTuning { | ||
| static let qualifiedQueryBlockSize = 512 | ||
| static let qualifiedMachineModel = "Mac16,5" | ||
| static let qualifiedMemoryGB: UInt64 = 128 | ||
| static let qualifiedGPUCores: UInt32 = 40 | ||
|
|
||
| static let currentMachineQualifiesForWideComposedPrefill: Bool = { | ||
| guard | ||
| (try? sysctlString("hw.model")) == qualifiedMachineModel, | ||
| (try? sysctlString("machdep.cpu.brand_string")) == "Apple M4 Max" | ||
| else { return false } | ||
| let bytesPerGB = UInt64(1024 * 1024 * 1024) | ||
| return ProcessInfo.processInfo.physicalMemory / bytesPerGB >= qualifiedMemoryGB | ||
|
Comment on lines
+12
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On a 128 GB AGENTS.md reference: docs/AGENTS.md:L19-L22 Useful? React with 👍 / 👎. |
||
| }() | ||
|
|
||
| static func policy( | ||
| isQwen35: Bool, | ||
| environment: [String: String], | ||
| hardware: HardwareInfo? | ||
| ) -> CBv2AttentionExecutionPolicy { | ||
| policy( | ||
| isQwen35: isQwen35, | ||
| environment: environment, | ||
| wideComposedPrefillQualified: hardware.map(qualifiesForWideComposedPrefill) == true) | ||
| } | ||
|
|
||
| static func currentMachinePolicy( | ||
| isQwen35: Bool, | ||
| environment: [String: String] | ||
| ) -> CBv2AttentionExecutionPolicy { | ||
| policy( | ||
| isQwen35: isQwen35, | ||
| environment: environment, | ||
| wideComposedPrefillQualified: | ||
| isQwen35 && currentMachineQualifiesForWideComposedPrefill) | ||
| } | ||
|
|
||
| private static func policy( | ||
| isQwen35: Bool, | ||
| environment: [String: String], | ||
| wideComposedPrefillQualified: Bool | ||
| ) -> CBv2AttentionExecutionPolicy { | ||
| let control = environment[CBv2AttentionExecutionPolicy.environmentVariable] | ||
| .flatMap { | ||
| CBv2AttentionExecutionControl( | ||
| rawValue: $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()) | ||
| } ?? .fallback | ||
|
|
||
| let queryBlockSize: Int | ||
| if let raw = environment[CBv2AttentionExecutionPolicy.queryBlockEnvironmentVariable] { | ||
| queryBlockSize = Int(raw).flatMap { $0 >= 0 ? $0 : nil } ?? 128 | ||
| } else if isQwen35, wideComposedPrefillQualified { | ||
| queryBlockSize = qualifiedQueryBlockSize | ||
| } else { | ||
| queryBlockSize = 128 | ||
| } | ||
|
|
||
| // No architecture has qualified the fused D256 kernel as a speed route. | ||
| // Explicit `fused` remains available as the bounded-memory A/B arm; | ||
| // `auto` therefore fails closed until a future hardware qualification. | ||
| return CBv2AttentionExecutionPolicy( | ||
| control: control, | ||
| hardwareQualification: nil, | ||
| fallbackQueryBlockSize: queryBlockSize) | ||
| } | ||
|
|
||
| static func qualifiesForWideComposedPrefill(_ hardware: HardwareInfo) -> Bool { | ||
| hardware.machineModel == qualifiedMachineModel | ||
| && hardware.chipFamily == .m4 | ||
| && hardware.chipTier == .max | ||
| && hardware.memoryGb == qualifiedMemoryGB | ||
| && hardware.gpuCores == qualifiedGPUCores | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These public behavioral claims about automatic qL selection and fused execution provide no canonical source paths or line references. Add citations to the production tuning and factory wiring so the report remains traceable to the implementation, as required for documentation that describes behavior.
AGENTS.md reference: docs/AGENTS.md:L19-L22
Useful? React with 👍 / 👎.