Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
10 changes: 8 additions & 2 deletions .github/workflows/release-swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -708,17 +708,23 @@ jobs:
test -s "$FINAL_APP_METALLIB"
test -s "$FINAL_FLAT_METALLIB"
cmp "$FINAL_FLAT_METALLIB" "$FINAL_APP_METALLIB"
# Repeat the helper's exact four-part completeness contract against
# Repeat the helper's completeness contract against
# the final extracted, signed bytes rather than the pre-signing input.
NAX_SYMBOL="_nax"
GEMV_SYMBOL="gemv"
R1_BUILDER_SYMBOL="build_gemma4_sorted_expert_tiles_bm32"
R1_BUILDER_E256_SYMBOL="build_sorted_expert_tiles_bm32_e256"
R1_KERNEL_SYMBOL="affine_gather_qmm_gemma4_expert_tiles_bfloat16_t_gs_64_b_4_alN_true_bm_32_bn_32_bk_32"
SDPA_D256_BF16_SYMBOL="steel_attention_bfloat16_bq32_bk16_bd256_wm4_wn1_maskbfloat16"
SDPA_D256_FP16_SYMBOL="steel_attention_float16_bq32_bk16_bd256_wm4_wn1_maskfloat16"
for symbol in \
"$NAX_SYMBOL" \
"$GEMV_SYMBOL" \
"$R1_BUILDER_SYMBOL" \
"$R1_KERNEL_SYMBOL"
"$R1_BUILDER_E256_SYMBOL" \
"$R1_KERNEL_SYMBOL" \
"$SDPA_D256_BF16_SYMBOL" \
"$SDPA_D256_FP16_SYMBOL"
do
MATCHES=$(strings "$FINAL_FLAT_METALLIB" | grep -F -c "$symbol" || true)
if [ "$MATCHES" -eq 0 ]; then
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Changelog

## Unreleased (v0.8.5 candidate - provider)
## Unreleased (v0.8.6 candidate - provider)

### Provider (Swift)

#### Performance

- **Qwen3.6 M4 Max wide-prefill route + explicit fused D256 control** - Uses the measured composed qL=512 path only on the qualified 40-core, 128 GB M4 Max (`Mac16,5`), retaining qL=128 elsewhere and honoring the existing override. On the production CBv2 path, paired release measurements improve median TTFT **2.4% at 8k and 3.7% at 32k (1.22 s)** for only +40.6 MiB (+2.2%) transient memory at 32k. Adds ABI-safe `force_fused` from MLX through mlx-c/Swift and a source-matched BF16/F16 D256 Steel kernel as an explicit bounded-memory arm: at 32k it reduces transient memory 15.5% versus qL=512 but is slower, so it remains opt-in and `auto` fails closed. Four faster-kernel designs (Q8 head-sharded, Q32/D128-sharded, two-head GQA, eight-head GQA) were correct but lost the composed-path gate and are not shipped.
- **Qwen3.6 E=256 expert-tile prefill route + fused gate_up** - Instantiates the Gemma4 descriptor/tile kernel family for Qwen's 256-expert shapes (mlx `d3c82db`), fuses the routed gate/up projection into one gather (`SwitchGLU(fuseGateUp: true)`, per-layer and per-load with heterogeneous-quantization split fallback across every checkpoint key space), and adds the opt-in `trust` refinement that skips the per-chunk retract drain. Measured on M4 Max, prod artifact: routed MoE block -26.3% at T=512; end-to-end prefill 1243→1364 tok/s default, 1433 with `trust` (+15.2%) at 8k; 2k +7.4%, 32k +6.6%. (#617, mlx-swift-lm#107)
- **Qwen3.6 MTP: GDN capture-verify, target_prefix acceptance, single-forward drafts** *(behind the `mtp` beta flag, default off)* - Replaces the serial target verify (measured 0.70x vs MTP-off) with one `[1,1+k]` forward capturing per-position recurrent states (1.08x), lifts the `temperature == 0` eligibility gate via pre-sampled target-token acceptance (exact at any temperature; temp-0.7 speculates at 0.70-0.77 acceptance), and defers the draft pair to eliminate the second MTP-head forward + 286 MB lm_head re-read per round: **116.5 tok/s vs 104.8 MTP-off (1.11-1.13x) at 0.78 acceptance**, B=1 greedy, release, paired sessions. (#616, mlx-swift-lm#106/#108)
- **mlx gpu::eval use-after-free fix** - A stale `MTL::CommandBuffer` captured across `eval_gpu` could crash any primitive that syncs mid-eval (deterministic SIGSEGV on the E=256 route; previously survived on allocator luck). (mlx#5/#7)
Expand Down
2 changes: 1 addition & 1 deletion coordinator/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ func keyLimitResetFromContext(ctx context.Context) string {
// the provider's EngineV2Factory.prepareProductionBackend for the argument).
// Keep this fallback in sync with ProviderCore.version so dev/in-memory
// coordinators advertise the same floor as the Swift binary they expect.
var LatestProviderVersion = "0.8.5"
var LatestProviderVersion = "0.8.6"

// minProviderVersionForDesiredModels is the first provider version whose Swift
// runtime understands the desired_models message. The coordinator must NOT send
Expand Down
96 changes: 96 additions & 0 deletions docs/reports/2026-08-17-qwen36-head256-attention.md
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.
Comment on lines +26 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add canonical code citations to the behavior claims

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 👍 / 👎.


## 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
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Record the attention arm in benchmark evidence

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
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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),
Expand All @@ -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
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -509,6 +510,7 @@ extension EngineV2Factory {
kvBackend: kvBackend,
maxContextLength: maxContextLength,
environment: environment,
attentionExecutionPolicy: attentionExecutionPolicy,
pagedPreflightOverride: pagedPreflightOverride)
return try assembleProductionBuild(
model: model,
Expand All @@ -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 {
Expand All @@ -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) }
Expand All @@ -575,6 +582,11 @@ extension EngineV2Factory {
String(describing: type(of: model)))
}

let resolvedAttentionExecutionPolicy = attentionExecutionPolicy
?? QwenHead256AttentionTuning.currentMachinePolicy(
isQwen35: isQwen35,
Comment on lines 582 to +587

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate the wide block on the D256 shape

On the qualified machine, every Qwen35MoEModel receives qL=512 regardless of its attention head dimension, although this change qualifies only the D256 Qwen3.6 artifact. The accepted qwen3_5_moe family is not shape-invariant—the repository's production-factory fixture uses the same model class with head_dim: 64—so loading another supported Qwen3.5/3.6 target silently applies the unmeasured wider block instead of retaining qL=128. Derive eligibility from the model's layer kinds/head dimension rather than the family type alone.

Useful? React with 👍 / 👎.

environment: environment)

var resolvedKind: EngineV2KVBackendKind
switch kvBackend {
case .contiguous: resolvedKind = .contiguous
Expand Down Expand Up @@ -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,
Expand Down
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the GPU-core qualification in production

On a 128 GB Mac16,5 M4 Max with 32 GPU cores—the explicit negative case in QwenHead256AttentionTuningTests.unqualifiedHardware—this production predicate still returns true because it checks only the model identifier, brand, and memory. Such a provider therefore receives the unqualified qL=512 policy even though the test and release report require qL=128 outside the exact 40-core configuration; use the same full HardwareInfo qualification on the production path or otherwise verify the GPU-core count.

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
}
}
5 changes: 4 additions & 1 deletion provider-swift/Sources/ProviderCore/ProviderCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,5 +219,8 @@ public enum ProviderCore {
// target_prefix + single-forward drafts (1.13x, behind the mtp beta
// flag), symlink-proof inline-MTP inspection, MTP /metrics counters,
// and hardened plaintext egress paths.
public static let version = "0.8.5"
// 0.8.6 adds the M4 Max Qwen3.6 wide-prefill route and an explicit,
// ABI-safe fused D256 bounded-memory control. Automatic fused selection
// stays closed because composed attention remains faster on M4 Max.
public static let version = "0.8.6"
}
Loading
Loading