From 6e687b355f03a7527776abcf5d39e37d41b98c28 Mon Sep 17 00:00:00 2001 From: CodeAndCanvas728 Date: Sun, 4 Oct 2026 17:00:52 +0200 Subject: [PATCH 1/2] feat: --no-vision loads a vision-capable checkpoint as text-only VLM auto-detection loads checkpoints that ship a vision_config (Qwen3.5/3.6 MoE among them) through the VLM factory, and the prompt cache is skipped for every VLM-loaded model (skipPromptCache includes isVLM). A text-only coding agent on such a checkpoint therefore re-prefills its whole history on every turn. --no-vision skips auto-detection and loads the text LLM, so the hybrid prompt cache applies. On a 16 GB M2 with a 35B-A3B qwen3_5_moe and --stream-experts, turn 2 of an agent session (14.7k tokens) drops from 166.6 s to 31.7 s and turn 3 (16.5k) from 189.0 s to 29.7 s. --vision and --no-vision are mutually exclusive. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014J4GsqznbKNX8tcKzNRxEr --- README.md | 1 + Sources/SwiftLM/Server.swift | 15 ++++++++++++++- tests/SwiftLMTests/NoVisionFlagTests.swift | 16 ++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/SwiftLMTests/NoVisionFlagTests.swift diff --git a/README.md b/README.md index 6bf11ab..0b45793 100644 --- a/README.md +++ b/README.md @@ -629,6 +629,7 @@ curl http://localhost:5413/v1/chat/completions \ | `--host` | `127.0.0.1` | Host to bind | | `--vision` | `false` | Enable VLM (vision-language model) mode for image inputs | | `--audio` | `false` | Enable ALM (audio-language model) mode for audio inputs | +| `--no-vision` | `false` | Load a vision-capable checkpoint as a text-only LLM, skipping VLM auto-detection. Text-only workloads (coding agents) then get the prompt cache, which is skipped for VLM-loaded models. Mutually exclusive with `--vision` | | `--max-tokens` | `2048` | Max tokens limit per generation | | `--prefill-size`| `512` | Prompt prefill chunk size (micro-batching for long contexts) | | `--top-p` | `1.0` | Default top-p nucleus sampling (overridable per-request) | diff --git a/Sources/SwiftLM/Server.swift b/Sources/SwiftLM/Server.swift index bd7225d..807949a 100644 --- a/Sources/SwiftLM/Server.swift +++ b/Sources/SwiftLM/Server.swift @@ -561,6 +561,9 @@ struct MLXServer: AsyncParsableCommand { @Flag(name: .long, help: "Enable VLM (vision-language model) mode for image inputs") var vision: Bool = false + @Flag(name: .long, help: "Load a vision-capable checkpoint as a text-only LLM, skipping VLM auto-detection. Text-only workloads then get the prompt cache, which is skipped for VLM-loaded models") + var noVision: Bool = false + @Flag(name: .long, help: "Enable ALM (audio-language model) mode for audio inputs") var audio: Bool = false @@ -615,6 +618,12 @@ struct MLXServer: AsyncParsableCommand { @Option(name: .long, help: "Assistant checkpoint providing MTP heads, for model families that ship them separately instead of in the main checkpoint (Gemma 4). Ignored when the main model carries its own MTP heads (Qwen3.5, DeepSeek V4).") var mtpAssistantModel: String? + func validate() throws { + if vision && noVision { + throw ValidationError("--vision and --no-vision are mutually exclusive.") + } + } + /// Entry point. Delegates to `runInner`, then — on any thrown error, at any /// stage — self-reports it as an `exiting` event before rethrowing, so /// process exit status is unchanged but the daemon gets a positive signal @@ -975,12 +984,16 @@ struct MLXServer: AsyncParsableCommand { // speculative-decoding suite. --vision remains a valid explicit override. let speculativeDecodingRequested = self.draftModel != nil || self.dflash || self.mtp let autoDetectedVision = !self.audio && architecture.supportsVision - && !speculativeDecodingRequested + && !speculativeDecodingRequested && !self.noVision var isVision = self.vision || autoDetectedVision if architecture.supportsVision, !self.vision, !self.audio, speculativeDecodingRequested { print( "[SwiftLM] Note: \(architecture.modelType ?? "unknown") reports vision support, but speculative/MTP decoding was requested; loading as a text-only LLM." ) + } else if architecture.supportsVision, self.noVision { + print( + "[SwiftLM] Note: \(architecture.modelType ?? "unknown") reports vision support, but --no-vision was given; loading as a text-only LLM." + ) } else if autoDetectedVision { print( "[SwiftLM] Auto-detected VLM config (\(architecture.modelType ?? "unknown")); enabling vision mode." diff --git a/tests/SwiftLMTests/NoVisionFlagTests.swift b/tests/SwiftLMTests/NoVisionFlagTests.swift new file mode 100644 index 0000000..8d88324 --- /dev/null +++ b/tests/SwiftLMTests/NoVisionFlagTests.swift @@ -0,0 +1,16 @@ +import XCTest +@testable import SwiftLM + +/// `--no-vision` loads a vision-capable checkpoint as a text-only LLM, so text-only +/// workloads get the prompt cache (skipped for every VLM-loaded model). +final class NoVisionFlagTests: XCTestCase { + + func testNoVisionParses() throws { + XCTAssertTrue(try MLXServer.parse(["--model", "m", "--no-vision"]).noVision) + XCTAssertFalse(try MLXServer.parse(["--model", "m"]).noVision) + } + + func testVisionAndNoVisionAreMutuallyExclusive() { + XCTAssertThrowsError(try MLXServer.parse(["--model", "m", "--vision", "--no-vision"])) + } +} From ab1cce19c6001386dc03da30f196036b251d1087 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Sun, 4 Oct 2026 11:19:16 -0700 Subject: [PATCH 2/2] no-vision: gate log note on !audio; refresh prompt-cache wording after #202 Gemma 4 VLM text-only requests are now cached, so the flag only matters for other VLM families. Co-Authored-By: Claude Sonnet 5.5 --- README.md | 2 +- Sources/SwiftLM/Server.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dfee788..68779a5 100644 --- a/README.md +++ b/README.md @@ -629,7 +629,7 @@ curl http://localhost:5413/v1/chat/completions \ | `--host` | `127.0.0.1` | Host to bind | | `--vision` | `false` | Enable VLM (vision-language model) mode for image inputs | | `--audio` | `false` | Enable ALM (audio-language model) mode for audio inputs | -| `--no-vision` | `false` | Load a vision-capable checkpoint as a text-only LLM, skipping VLM auto-detection. Text-only workloads (coding agents) then get the prompt cache, which is skipped for VLM-loaded models. Mutually exclusive with `--vision` | +| `--no-vision` | `false` | Load a vision-capable checkpoint as a text-only LLM, skipping VLM auto-detection. Text-only workloads (coding agents) then get the prompt cache, which is still skipped for most VLM-loaded models (Gemma 4 text-only requests are already cached, so the flag is unnecessary there). Mutually exclusive with `--vision` | | `--max-tokens` | `2048` | Max tokens limit per generation | | `--prefill-size`| `512` | Prompt prefill chunk size (micro-batching for long contexts) | | `--top-p` | `1.0` | Default top-p nucleus sampling (overridable per-request) | diff --git a/Sources/SwiftLM/Server.swift b/Sources/SwiftLM/Server.swift index abffce7..750e76d 100644 --- a/Sources/SwiftLM/Server.swift +++ b/Sources/SwiftLM/Server.swift @@ -564,7 +564,7 @@ struct MLXServer: AsyncParsableCommand { @Flag(name: .long, help: "Enable VLM (vision-language model) mode for image inputs") var vision: Bool = false - @Flag(name: .long, help: "Load a vision-capable checkpoint as a text-only LLM, skipping VLM auto-detection. Text-only workloads then get the prompt cache, which is skipped for VLM-loaded models") + @Flag(name: .long, help: "Load a vision-capable checkpoint as a text-only LLM, skipping VLM auto-detection. Text-only workloads then get the prompt cache, which is still skipped for most VLM-loaded models (Gemma 4 text-only requests are cached)") var noVision: Bool = false @Flag(name: .long, help: "Enable ALM (audio-language model) mode for audio inputs") @@ -993,7 +993,7 @@ struct MLXServer: AsyncParsableCommand { print( "[SwiftLM] Note: \(architecture.modelType ?? "unknown") reports vision support, but speculative/MTP decoding was requested; loading as a text-only LLM." ) - } else if architecture.supportsVision, self.noVision { + } else if architecture.supportsVision, !self.audio, self.noVision { print( "[SwiftLM] Note: \(architecture.modelType ?? "unknown") reports vision support, but --no-vision was given; loading as a text-only LLM." )