292 lines
8.4 KiB
Markdown
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.
|