From ec3d19a52ef690fb82609d1235820f5932149976 Mon Sep 17 00:00:00 2001 From: Luke <1255926+notatestuser@users.noreply.github.com> Date: Thu, 17 Sep 2026 07:46:51 +0800 Subject: [PATCH 1/2] feat: allow V4.1 Flash 41-q2 on 96 GiB Macs and default to V4.1 there - flash41MinRamGiB drops 41-q2's floor from 128 to 96 GiB. Verified against the pinned ds4 admission with the real Q2 weights (--memory-plan): a 96 GiB host with the ~72 GiB Metal recommended set admits the streaming config with a 4,827-expert cache at 32K ctx (3,638 at 1M), and rejects only below the 38.3 GiB fixed set. - Fresh installs on 96 GiB now default to V4.1 Flash (q2, ctx 32,768); the popup variant picker and Settings quant picker offer it from 96 GiB. - 41-q4's 256 GiB floor and the 128 GiB Max Think floor are unchanged; README tier table, AGENTS harness notes, and the harness header updated. --- AGENTS.md | 5 +- README.md | 2 +- Sources/DS4Control/AppState.swift | 4 +- Sources/DS4Control/Model/Feasibility.swift | 12 +++-- Sources/DS4Control/Views/ModelRowView.swift | 2 +- Tests/DS4ControlTests/AppStateTests.swift | 15 ++++-- Tests/DS4ControlTests/FeasibilityTests.swift | 47 +++++++++++++++++-- .../GUIHostOptionSourceTests.swift | 2 +- scripts/flash41-mem-harness.sh | 3 +- 9 files changed, 70 insertions(+), 22 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 159eb06..e5e4b60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,8 +72,9 @@ disown - `scripts/flash-mem-harness.sh` — manual harness that boots the real Flash 0731 model at various context sizes and samples resident memory (NOT part of `swift test`; loads ~81 GB). - `scripts/flash41-mem-harness.sh` — the V4.1 equivalent: `--ssd-streaming --power 100` on a - 128 GiB+ Mac, cross-checks ds4's `ds4: memory:` plan against the `Feasibility` mirror (loads a - 341 GiB GGUF; Engram rows stay on disk). + 96 GiB+ Mac (set `DS41_LIMIT_GIB` to the machine's RAM; default 128), cross-checks ds4's + `ds4: memory:` plan against the `Feasibility` mirror (loads a 341 GiB GGUF; Engram rows stay + on disk). - CI: `.github/workflows/ci.yml` (build + test, bundles ds4), `release.yml` (tag-triggered Developer ID signed + notarized release). diff --git a/README.md b/README.md index 0c799a0..479f412 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ DeepSeek V4 is memory-hungry so DS4 Control gates feasibility before launching. | Variant | Quant | RAM | Notes | | --- | --- | --- | --- | | V4 Pro (0813) | pro-imatrix | **≥ 512 GiB required** | Anything below is blocked. | -| V4.1 Flash | 41-q2 | ≥ 128 GiB | 341 GiB on disk, ~152 GiB resident main weights + ~189 GiB disk-only Engram. SSD streaming engages automatically on 128–255 GiB; full residency on ≥ 256 GiB. | +| V4.1 Flash | 41-q2 | ≥ 96 GiB | 341 GiB on disk, ~152 GiB resident main weights + ~189 GiB disk-only Engram. SSD streaming engages automatically on 96–255 GiB (slow: nearly every routed expert streams from disk); full residency on ≥ 256 GiB. | | V4.1 Flash | 41-q4 | ≥ 256 GiB | 483 GiB on disk, ~294 GiB resident main weights. SSD streaming on 256–511 GiB; full residency on ≥ 512 GiB. | | V4 Flash (0731) | q4-imatrix | ≥ 256 GiB | Standard. | | V4 Flash (0731) | q2-imatrix | 96 GiB minimum | 96–127 GiB requires raising the Metal wired limit (see below). | diff --git a/Sources/DS4Control/AppState.swift b/Sources/DS4Control/AppState.swift index 8a250d8..901f7f7 100644 --- a/Sources/DS4Control/AppState.swift +++ b/Sources/DS4Control/AppState.swift @@ -109,9 +109,9 @@ final class AppState: ObservableObject { } legacyStoragePromptDismissed = d.bool(forKey: "legacyStoragePromptDismissed0813") let stored = d.string(forKey: "selectedVariant").flatMap(Variant.init(rawValue:)) - // Default Pro on ≥512 GiB, V4.1 Flash on ≥128 GiB, V4 Flash (0731) below that. + // Default Pro on ≥512 GiB, V4.1 Flash on ≥96 GiB, V4 Flash (0731) below that. // Stored selections are never auto-migrated: an existing 0731 user keeps 0731. - selectedVariant = stored ?? (ramGiB >= 512 ? .pro : ramGiB >= 128 ? .flash41 : .flash) + selectedVariant = stored ?? (ramGiB >= 512 ? .pro : ramGiB >= 96 ? .flash41 : .flash) let storedQuant = d.string(forKey: "selectedFlashQuant").flatMap(FlashQuant.init(rawValue:)) selectedFlashQuant = storedQuant ?? defaultFlashQuant(ramGiB: ramGiB) // default q2-q4-imatrix let storedQuant41 = d.string(forKey: "selectedFlash41Quant").flatMap(Flash41Quant.init(rawValue:)) diff --git a/Sources/DS4Control/Model/Feasibility.swift b/Sources/DS4Control/Model/Feasibility.swift index b6c039a..49ed89a 100644 --- a/Sources/DS4Control/Model/Feasibility.swift +++ b/Sources/DS4Control/Model/Feasibility.swift @@ -563,10 +563,12 @@ func flash41UsesSSDStreaming( return resident > flash41BudgetMB(ramGiB: ramGiB, wiredLimitMB: wiredLimitMB) } -/// Documented RAM floor per V4.1 quant (README tier table): 41-q2 runs from 128 GiB via SSD -/// streaming; 41-q4 requires ≥ 256 GiB — the 128 GiB class would have to stream essentially -/// every routed expert, which the supported tiers do not offer. -func flash41MinRamGiB(_ q: Flash41Quant) -> Double { q == .q4 ? 256 : 128 } +/// Documented RAM floor per V4.1 quant (README tier table): 41-q2 runs from 96 GiB via SSD +/// streaming — verified against the pinned ds4 admission with the real Q2 weights, which +/// admits a 96 GiB host (3,638 cached experts at a 72 GiB Metal recommended set and 1M ctx) +/// — and 41-q4 requires ≥ 256 GiB: below that it would have to stream essentially every +/// routed expert, which the supported tiers do not offer. +func flash41MinRamGiB(_ q: Flash41Quant) -> Double { q == .q4 ? 256 : 96 } /// Whether a V4.1 quant's default launch fits this machine. Drives which options the /// Settings quant picker offers; the RAM floor is the documented tier, then the launch's @@ -717,7 +719,7 @@ func feasibility( return .blocked( reason: q == .q4 ? "V4.1 Flash 41-q4 needs ≥ 256 GiB unified memory. Its 294 GiB of main weights would stream essentially every routed expert below that tier, which is not supported." - : "V4.1 Flash needs ≥ 128 GiB unified memory. Its 152 GiB of resident main weights, plus disk-only Engram streaming, cannot run safely below that." + : "V4.1 Flash needs ≥ 96 GiB unified memory. Below that, even the SSD-streaming fixed set (non-routed weights, graph, and prefill headroom) plus a routed expert cannot be admitted." ) } } diff --git a/Sources/DS4Control/Views/ModelRowView.swift b/Sources/DS4Control/Views/ModelRowView.swift index 32d2325..bf7138f 100644 --- a/Sources/DS4Control/Views/ModelRowView.swift +++ b/Sources/DS4Control/Views/ModelRowView.swift @@ -8,7 +8,7 @@ struct ModelRowView: View { private var variants: [Variant] { if ramGiB >= 512 { return [.pro, .flash, .flash41] } - if ramGiB >= 128 { return [.flash, .flash41] } + if ramGiB >= 96 { return [.flash, .flash41] } return [.flash] } diff --git a/Tests/DS4ControlTests/AppStateTests.swift b/Tests/DS4ControlTests/AppStateTests.swift index 9b3da51..01e1ebf 100644 --- a/Tests/DS4ControlTests/AppStateTests.swift +++ b/Tests/DS4ControlTests/AppStateTests.swift @@ -131,6 +131,10 @@ final class AppStateTests: XCTestCase { XCTAssertEqual(app.thinkingMode, .standard) XCTAssertEqual(app.ctxOverride, 0) + // 96 GiB now defaults to V4.1 Flash (streaming → 32,768); the 0731 tier still + // defaults to 256,000 there. + XCTAssertEqual(app.effectiveCtx(ramGiB: 96), 32_768) + app.selectedVariant = .flash XCTAssertEqual(app.effectiveCtx(ramGiB: 96), 256_000) XCTAssertEqual(defaults.string(forKey: "thinkingMode"), ThinkingMode.standard.rawValue) XCTAssertEqual(defaults.integer(forKey: "ctxOverride"), 0) @@ -169,13 +173,16 @@ final class AppStateTests: XCTestCase { XCTAssertEqual(app2.thinkingMode, .max) } - func testFreshInstallDefaultsFlash41On128GiB() { + func testFreshInstallDefaultsFlash41From96GiB() { + let a96 = AppState( + defaults: UserDefaults(suiteName: "test.\(UUID().uuidString)")!, ramGiB: 96) + XCTAssertEqual(a96.selectedVariant, .flash41) let a128 = AppState( defaults: UserDefaults(suiteName: "test.\(UUID().uuidString)")!, ramGiB: 128) XCTAssertEqual(a128.selectedVariant, .flash41) - let a96 = AppState( - defaults: UserDefaults(suiteName: "test.\(UUID().uuidString)")!, ramGiB: 96) - XCTAssertEqual(a96.selectedVariant, .flash) + let a64 = AppState( + defaults: UserDefaults(suiteName: "test.\(UUID().uuidString)")!, ramGiB: 64) + XCTAssertEqual(a64.selectedVariant, .flash) let a512 = AppState( defaults: UserDefaults(suiteName: "test.\(UUID().uuidString)")!, ramGiB: 512) XCTAssertEqual(a512.selectedVariant, .pro) diff --git a/Tests/DS4ControlTests/FeasibilityTests.swift b/Tests/DS4ControlTests/FeasibilityTests.swift index b20263e..1b0d898 100644 --- a/Tests/DS4ControlTests/FeasibilityTests.swift +++ b/Tests/DS4ControlTests/FeasibilityTests.swift @@ -364,12 +364,18 @@ final class FeasibilityTests: XCTestCase { } func testFlash41FloorsAndGates() { + if case .standard = feasibility( + ramGiB: 96, selection: .flash41(.q2), ctx: 32_768, wiredLimitMB: 73_728) + { + } else { + XCTFail("V4.1 q2 on 96 GiB with streaming must pass (verified against ds4 admission)") + } if case let .blocked(reason) = feasibility( - ramGiB: 96, selection: .flash41(.q2), ctx: 32_768, wiredLimitMB: 86_016) + ramGiB: 64, selection: .flash41(.q2), ctx: 32_768, wiredLimitMB: 61_440) { - XCTAssertTrue(reason.contains("128 GiB")) + XCTAssertTrue(reason.contains("96 GiB")) } else { - XCTFail("V4.1 below 128 GiB must block") + XCTFail("V4.1 below 96 GiB must block") } if case .standard = feasibility( ramGiB: 128, selection: .flash41(.q2), ctx: 32_768, wiredLimitMB: 98_304) @@ -392,8 +398,38 @@ final class FeasibilityTests: XCTestCase { } } + /// The pinned ds4 admission at the app's lowest V4.1 tier: on a 96 GiB host with the + /// macOS-default ~72 GiB Metal set, the streamed fixed set leaves room for a routed + /// expert, so the launch is admitted (verified directly against `--memory-plan` with the + /// real Q2 weights: 4,827 cached experts at 32K ctx, 3,638 at 1M). + func testFlash41Q2AdmittedOn96GiBViaStreaming() { + let wired = 72 * 1024 + XCTAssertTrue( + flash41UsesSSDStreaming( + ramGiB: 96, wiredLimitMB: wired, quant: .q41Q2, ctx: 32_768, sessions: 1)) + XCTAssertEqual( + feasibility( + ramGiB: 96, selection: .flash41(.q2), ctx: 32_768, wiredLimitMB: wired), + .standard) + XCTAssertEqual( + feasibility( + ramGiB: 96, selection: .flash41(.q2), ctx: 1_048_576, wiredLimitMB: wired), + .standard) + // The streamed fixed set leaves ~46 GiB under the Metal set — far more than one + // routed layer's experts — so ds4's admission (mirrored here) holds. + let fixed = v41FixedWiredMB(quant: .q41Q2, ctx: 32_768, sessions: 1, streaming: true) + XCTAssertEqual(fixed, 27_007) + XCTAssertEqual(flash41BudgetMB(ramGiB: 96, wiredLimitMB: wired), wired) + XCTAssertGreaterThan(wired - fixed, ds41Q2PrefillHeadroomBytes / 1_048_576 / 2) + // Parallel slots: the 32K graph fits six sessions under the default Metal set. + XCTAssertEqual( + maxFittingSessions( + ramGiB: 96, selection: .flash41(.q2), ctx: 32_768, wiredLimitMB: wired), 6) + } + func testFlash41QuantFitFloors() { - XCTAssertFalse(flash41QuantFits(.q2, ramGiB: 96, wiredLimitMB: Int.max)) + XCTAssertTrue(flash41QuantFits(.q2, ramGiB: 96, wiredLimitMB: 73_728)) + XCTAssertFalse(flash41QuantFits(.q2, ramGiB: 64, wiredLimitMB: Int.max)) XCTAssertFalse(flash41QuantFits(.q4, ramGiB: 96, wiredLimitMB: Int.max)) XCTAssertTrue(flash41QuantFits(.q2, ramGiB: 128, wiredLimitMB: 98_304)) // 41-q4's documented floor is 256 GiB (README tier table): the 128 GiB class streams @@ -404,8 +440,9 @@ final class FeasibilityTests: XCTestCase { } func testFlash41DefaultCtxAndQuant() { - // Streaming tiers keep upstream's documented 32,768 default: 41-q2 on 128 GiB, and + // Streaming tiers keep upstream's documented 32,768 default: 41-q2 from 96 GiB, and // 41-q4 through its 256 GiB floor (the 1M window doesn't fit fully resident yet). + XCTAssertEqual(defaultCtx(ramGiB: 96, selection: .flash41(.q2)), 32_768) XCTAssertEqual(defaultCtx(ramGiB: 128, selection: .flash41(.q2)), 32_768) XCTAssertEqual(defaultCtx(ramGiB: 256, selection: .flash41(.q4)), 32_768) // Tiers that hold the full 1M window resident default to the ceiling instead. diff --git a/Tests/DS4ControlTests/GUIHostOptionSourceTests.swift b/Tests/DS4ControlTests/GUIHostOptionSourceTests.swift index 9c4b365..2943417 100644 --- a/Tests/DS4ControlTests/GUIHostOptionSourceTests.swift +++ b/Tests/DS4ControlTests/GUIHostOptionSourceTests.swift @@ -229,7 +229,7 @@ final class GUIHostOptionSourceTests: XCTestCase { let modelRow = try source("Sources/DS4Control/Views/ModelRowView.swift") XCTAssertTrue(modelRow.contains("if ramGiB >= 512 { return [.pro, .flash, .flash41] }")) - XCTAssertTrue(modelRow.contains("if ramGiB >= 128 { return [.flash, .flash41] }")) + XCTAssertTrue(modelRow.contains("if ramGiB >= 96 { return [.flash, .flash41] }")) XCTAssertTrue(modelRow.contains("return [.flash]")) } } diff --git a/scripts/flash41-mem-harness.sh b/scripts/flash41-mem-harness.sh index 6a216c2..e537e50 100755 --- a/scripts/flash41-mem-harness.sh +++ b/scripts/flash41-mem-harness.sh @@ -1,6 +1,7 @@ #!/bin/sh # flash41-mem-harness.sh — measure V4.1 Flash (Q2) resident memory across context sizes on a -# 128 GiB+ Mac. Spins up the REAL ds4-server with --ssd-streaming, warms the model, prefills to +# 96 GiB+ Mac (96 GiB is the app's lowest SSD-streaming tier for 41-q2). Spins up the REAL +# ds4-server with --ssd-streaming, warms the model, prefills to # the configured context frontier, and records peak RSS plus ds4's own memory plan. # # The GGUF carries ~189 GiB of disk-only Engram tables that never become resident; only the From 51875e25c6280576871d738bde956e6263ac53e0 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 23:59:35 +0000 Subject: [PATCH 2/2] docs: document memory-tier defaults and V4.1 tests --- Sources/DS4Control/AppState.swift | 1 + Sources/DS4Control/Views/ModelRowView.swift | 1 + Tests/DS4ControlTests/AppStateTests.swift | 2 ++ Tests/DS4ControlTests/FeasibilityTests.swift | 3 +++ Tests/DS4ControlTests/GUIHostOptionSourceTests.swift | 1 + 5 files changed, 8 insertions(+) diff --git a/Sources/DS4Control/AppState.swift b/Sources/DS4Control/AppState.swift index 901f7f7..db44966 100644 --- a/Sources/DS4Control/AppState.swift +++ b/Sources/DS4Control/AppState.swift @@ -67,6 +67,7 @@ final class AppState: ObservableObject { } } + /// Restores persisted preferences and chooses memory-appropriate defaults for a fresh install. init(defaults: UserDefaults = .standard, ramGiB: Double = systemRamGiB()) { self.d = defaults port = d.object(forKey: "port") as? Int ?? 8000 diff --git a/Sources/DS4Control/Views/ModelRowView.swift b/Sources/DS4Control/Views/ModelRowView.swift index bf7138f..008618d 100644 --- a/Sources/DS4Control/Views/ModelRowView.swift +++ b/Sources/DS4Control/Views/ModelRowView.swift @@ -6,6 +6,7 @@ struct ModelRowView: View { @Environment(\.openWindow) private var openWindow let ramGiB: Double + /// Model variants offered by the popup for the machine's unified-memory tier. private var variants: [Variant] { if ramGiB >= 512 { return [.pro, .flash, .flash41] } if ramGiB >= 96 { return [.flash, .flash41] } diff --git a/Tests/DS4ControlTests/AppStateTests.swift b/Tests/DS4ControlTests/AppStateTests.swift index 01e1ebf..a37300a 100644 --- a/Tests/DS4ControlTests/AppStateTests.swift +++ b/Tests/DS4ControlTests/AppStateTests.swift @@ -122,6 +122,7 @@ final class AppStateTests: XCTestCase { XCTAssertEqual(AppState(defaults: off, ramGiB: 96).thinkingMode, .off) } + /// Max Think is downgraded safely while each 96 GiB model keeps its own default context. func testMaxThinkingModeDowngradesBelow128GiB() { let defaults = UserDefaults(suiteName: "test.\(UUID().uuidString)")! defaults.set(ThinkingMode.max.rawValue, forKey: "thinkingMode") @@ -173,6 +174,7 @@ final class AppStateTests: XCTestCase { XCTAssertEqual(app2.thinkingMode, .max) } + /// Fresh installs select the expected model at each supported unified-memory tier. func testFreshInstallDefaultsFlash41From96GiB() { let a96 = AppState( defaults: UserDefaults(suiteName: "test.\(UUID().uuidString)")!, ramGiB: 96) diff --git a/Tests/DS4ControlTests/FeasibilityTests.swift b/Tests/DS4ControlTests/FeasibilityTests.swift index 1b0d898..217d508 100644 --- a/Tests/DS4ControlTests/FeasibilityTests.swift +++ b/Tests/DS4ControlTests/FeasibilityTests.swift @@ -363,6 +363,7 @@ final class FeasibilityTests: XCTestCase { ramGiB: 512, wiredLimitMB: 393_216, quant: .q41Q4, ctx: 32_768, sessions: 1)) } + /// V4.1 quant tiers admit supported memory sizes and block hosts below their RAM floors. func testFlash41FloorsAndGates() { if case .standard = feasibility( ramGiB: 96, selection: .flash41(.q2), ctx: 32_768, wiredLimitMB: 73_728) @@ -427,6 +428,7 @@ final class FeasibilityTests: XCTestCase { ramGiB: 96, selection: .flash41(.q2), ctx: 32_768, wiredLimitMB: wired), 6) } + /// Quant availability follows the 96 GiB Q2 and 256 GiB Q4 minimum-memory tiers. func testFlash41QuantFitFloors() { XCTAssertTrue(flash41QuantFits(.q2, ramGiB: 96, wiredLimitMB: 73_728)) XCTAssertFalse(flash41QuantFits(.q2, ramGiB: 64, wiredLimitMB: Int.max)) @@ -439,6 +441,7 @@ final class FeasibilityTests: XCTestCase { XCTAssertTrue(flash41QuantFits(.q4, ramGiB: 256, wiredLimitMB: 196_608)) } + /// V4.1 defaults use streaming context below full-residency tiers and the expected quant. func testFlash41DefaultCtxAndQuant() { // Streaming tiers keep upstream's documented 32,768 default: 41-q2 from 96 GiB, and // 41-q4 through its 256 GiB floor (the 1M window doesn't fit fully resident yet). diff --git a/Tests/DS4ControlTests/GUIHostOptionSourceTests.swift b/Tests/DS4ControlTests/GUIHostOptionSourceTests.swift index 2943417..bc603dc 100644 --- a/Tests/DS4ControlTests/GUIHostOptionSourceTests.swift +++ b/Tests/DS4ControlTests/GUIHostOptionSourceTests.swift @@ -225,6 +225,7 @@ final class GUIHostOptionSourceTests: XCTestCase { XCTAssertLessThan(partRemoval.lowerBound, finalRemoval.lowerBound) } + /// The popup offers V4.1 Flash beginning at the supported 96 GiB memory tier. func testModelRowOffersFlash41ByRAMTier() throws { let modelRow = try source("Sources/DS4Control/Views/ModelRowView.swift")