Files

292 lines
8.4 KiB
Markdown

# MLXLMCommon
# Documentation
- [Porting and implementing models](https://swiftpackageindex.com/ml-explore/mlx-swift-lm/main/documentation/mlxlmcommon/porting)
- [MLXLLMCommon](https://swiftpackageindex.com/ml-explore/mlx-swift-lm/main/documentation/mlxlmcommon) -- common API for LLM and VLM
- [MLXLLM](https://swiftpackageindex.com/ml-explore/mlx-swift-lm/main/documentation/mlxllm) -- large language model example implementations
- [MLXVLM](https://swiftpackageindex.com/ml-explore/mlx-swift-lm/main/documentation/mlxvlm) -- vision language model example implementations
# Quick Start
Using LLMs and VLMs is as easy as:
```swift
import MLXLLM
import MLXLMHuggingFace
import MLXLMTokenizers
let model = try await loadModel(
from: HubClient.default,
using: TokenizersLoader(),
id: "mlx-community/Qwen3-4B-4bit"
)
let session = ChatSession(model)
print(try await session.respond(to: "What are two things to see in San Francisco?"))
print(try await session.respond(to: "How about a great place to eat?"))
```
## More Loading Scenarios
Load from a local directory:
```swift
import MLXLLM
import MLXLMTokenizers
let modelDirectory = URL(filePath: "/path/to/model")
let container = try await loadModelContainer(
from: modelDirectory,
using: TokenizersLoader()
)
```
Use a custom Hugging Face client:
```swift
import MLXLLM
import MLXLMHuggingFace
import MLXLMTokenizers
let hub = HubClient(token: "hf_...")
let container = try await loadModelContainer(
from: hub,
using: TokenizersLoader(),
id: "mlx-community/Qwen3-4B-4bit"
)
```
Use a custom downloader:
```swift
import MLXLLM
import MLXLMCommon
import MLXLMTokenizers
struct S3Downloader: Downloader {
func download(
id: String,
revision: String?,
matching patterns: [String],
useLatest: Bool,
progressHandler: @Sendable @escaping (Progress) -> Void
) async throws -> URL {
// Download files and return a local directory URL.
return URL(filePath: "/tmp/model")
}
}
let container = try await loadModelContainer(
from: S3Downloader(),
using: TokenizersLoader(),
id: "my-bucket/my-model"
)
```
For more information see
[Evaluation](https://swiftpackageindex.com/ml-explore/mlx-swift-lm/main/documentation/mlxlmcommon/evaluation)
or [Using Models](https://swiftpackageindex.com/ml-explore/mlx-swift-lm/main/documentation/mlxlmcommon/using-model)
for more advanced API.
# Contents
MLXLMCommon contains types and code that is generic across many types
of language models, from LLMs to VLMs:
- Evaluation
- KVCache
- Loading
- UserInput
## Loading a Model
A model is typically loaded by using a `ModelFactory` and a `ModelConfiguration`:
```swift
import MLXLMCommon
import MLXLMHuggingFace
import MLXLMTokenizers
// e.g. VLMModelFactory.shared
let modelFactory: ModelFactory
// e.g. VLMRegistry.paligemma3bMix4488bit
let modelConfiguration: ModelConfiguration
let container = try await modelFactory.loadContainer(
from: HubClient.default,
using: TokenizersLoader(),
configuration: modelConfiguration
)
// Custom Hub client (token, endpoint, etc.).
let customHub = HubClient(token: "hf_...")
let privateContainer = try await modelFactory.loadContainer(
from: customHub,
using: TokenizersLoader(),
configuration: modelConfiguration
)
```
The `container` provides an isolation context (an `actor`) to run inference in the model.
Predefined `ModelConfiguration` instances are provided as static variables
on the `ModelRegistry` types or they can be created:
```swift
let modelConfiguration = ModelConfiguration(id: "mlx-community/paligemma-3b-mix-448-8bit")
```
The flow inside the `ModelFactory` goes like this:
```swift
public class VLMModelFactory: ModelFactory {
public func _load(
configuration: ResolvedModelConfiguration,
tokenizerLoader: any TokenizerLoader
) async throws -> ModelContext {
// modelDirectory and tokenizerDirectory are already resolved
// load the base configuration
// using the typeRegistry create a model (random weights)
// load the weights, apply quantization as needed, update the model
// calls model.sanitize() for weight preparation
// load the tokenizer via tokenizerLoader.load(from: directory)
// (vlm) load the processor configuration, create the processor
}
}
```
Callers with specialized requirements can use these individual components to manually
load models, if needed.
## Evaluation Flow
- Load the Model
- UserInput
- LMInput
- generate()
- NaiveStreamingDetokenizer
- TokenIterator
## Using a Model
Once a model is loaded you can evaluate a prompt or series of
messages. Minimally you need to prepare the user input:
```swift
let prompt = "Describe the image in English"
var input = UserInput(prompt: prompt, images: image.map { .url($0) })
input.processing.resize = .init(width: 256, height: 256)
```
This example shows adding some images and processing instructions -- if
model accepts text only then these parts can be omitted. The inference
calls are the same.
Assuming you are using a `ModelContainer` (an actor that holds
a `ModelContext`, which is the bundled set of types that implement a
model), the first step is to convert the `UserInput` into the
`LMInput` (LanguageModel Input):
```swift
let generateParameters: GenerateParameters
let input: UserInput
let result = try await modelContainer.perform { [input] context in
let input = try context.processor.prepare(input: input)
```
Given that `input` we can call `generate()` to produce a stream
of tokens. In this example we use a `NaiveStreamingDetokenizer`
to assist in converting a stream of tokens into text and print it.
The stream is stopped after we hit a maximum number of tokens:
```
var detokenizer = NaiveStreamingDetokenizer(tokenizer: context.tokenizer)
return try MLXLMCommon.generate(
input: input, parameters: generateParameters, context: context
) { tokens in
if let last = tokens.last {
detokenizer.append(token: last)
}
if let new = detokenizer.next() {
print(new, terminator: "")
fflush(stdout)
}
if tokens.count >= maxTokens {
return .stop
} else {
return .more
}
}
}
```
### Wired Memory (Optional)
Use the policy-based API to coordinate a single global wired limit across tasks.
`WiredMemoryManager` and `WiredMemoryTicket` are provided by MLX, while
MLXLMCommon adds LLM-oriented policies (like `WiredFixedPolicy` or capped sum).
Policy-only admission is enabled by default on unsupported backends so the
same ticket logic applies on CPU (no OS limit changes are attempted).
```swift
let policy = WiredSumPolicy()
let ticket = policy.ticket(size: estimatedBytes)
let stream = try MLXLMCommon.generate(
input: input,
parameters: generateParameters,
context: context,
wiredMemoryTicket: ticket
)
```
Tickets are cheap handles into a shared manager that serializes updates and
restores the baseline when the last ticket completes.
For long-lived model weights, consider using a reservation ticket by passing
`kind: .reservation` when creating the ticket. Reservation tickets influence
admission and desired limits but do not keep the wired limit elevated unless
there is at least one active (inference) ticket.
#### Policies and Tickets
`WiredMemoryPolicy` is pure: it computes a desired limit from the baseline and
the active ticket sizes. The library includes a few policies:
- `WiredSumPolicy`: `baseline + sum(activeSizes)` with an optional cap.
- `WiredMaxPolicy`: `max(baseline, max(activeSizes))`.
- `WiredFixedPolicy`: fixed limit while any ticket is active.
Tickets are safe to start/end multiple times (extra ends are ignored). For
structured usage, wrap work with `WiredMemoryTicket.withWiredLimit` to ensure
start/end pairing and cancellation safety:
```swift
let policy = WiredSumPolicy()
let ticket = policy.ticket(size: estimatedBytes)
try await WiredMemoryTicket.withWiredLimit(ticket) {
// run inference
}
```
#### Admission Control (Optional)
Policies can also gate concurrency by overriding `canAdmit`. If admission is
denied, `start()` suspends until capacity is available and resumes when tickets
end. This helps prevent over-commit when many inferences launch at once.
#### Debug Event Stream
Use `WiredMemoryManager.events()` to observe policy stacking and limit changes
in DEBUG builds. The stream is empty in release builds, so event logging is a
no-op in production.