Creating Custom AI Models on a Macbook Pro
AI has changed the way we work, whether we’re developers or not. Understanding how LLMs work behind the scenes is a good skill to have, and it also helps you work with AI chat prompts in much more precise and useful ways.
What Are LLMs, Fine-tuning & RAG?
Many of the models used by popular tools like ChatGPT, Claude, and DeepSeek are actually available as open-source at huggingface.com. We can use these as a base model and supply them with specific training data to fine-tune the model to be good at specific tasks.
Different models are good for different things, much like the way we work. I’d expect a plumber to be great at fixing a leaking water main, but I wouldn’t expect them to be great at handling my taxes. The same goes for models, although the training data each uses isn’t quite as drastically different at this point. In the future, I do think highly specialized models will be the norm.
If you’ve ever had an LLM lie to you confidently, you’ve experienced an AI concept known as a hallucination. AI hallucinations happen when an LLM makes up information that sounds real but isn’t true. It’s like when the computer is trying to answer a question but doesn’t actually know the answer, so it guesses and tries to sound confident about its guess. Just like how a person might make up a story that sounds believable when they don’t know the real facts, AI can do the same thing without meaning to lie. It just doesn’t know the difference between what’s real and what it’s making up.
If you’ve experienced this, then you’ve found a great case for fine-tuning your own model. As a programmer, it’s common to have an LLM do this when you’re asking specific questions about something. I had it happen this week while generating some fish CLI scripts. Claude just didn’t have the specialized knowledge to answer properly.
In circumstances like this, it would be great to fill that gap yourself. If you could just give the AI bot the missing information, you know it could work. That’s what fine-tuning provides. With fine-tuning, you’re adjusting its parameters using additional training on specific data to make it better at particular tasks or domains. This process takes a pre-trained model and teaches it to respond in ways that are more aligned with your specific needs, whether that’s adopting a certain writing style, following particular instructions, or becoming more knowledgeable about specialized topics.
RAG, or retrieval-augmented generation, is another way to provide data to an LLM, but it’s different from fine-tuning. RAG retrieves and injects relevant information at inference time rather than permanently modifying the model ahead of time with training. With RAG, you search external databases or documents for relevant context and feed that information to the model alongside your prompt, while fine-tuning actually changes how the model behaves by updating its internal parameters through additional training.
Why Create a Custom Writing Assistant?
There are many circumstances where fine-tuning your own custom model makes a lot of sense, and creating a ghostwriter with your own voice is a simple and effective way to learn how to do it.
Let’s explore a real-world use case to understand how all this works. I’ve been writing blog posts at Kevinleary.net since 2009, and I continue to write today (that’s obvious). I use AI to assist with research for writing and to help correct grammar and spelling, but generating full posts with tools like ChatGPT or Claude never captures my voice. I can tweak and adjust, but I always end up rewriting a lot myself.
But what if I used all of my blog posts as data to fine-tune an LLM base model, like Llama 3.1 8B? Doing this will provide me with a specialized LLM trained on 10 years of my own writing style. The likelihood it will be able to match my own voice and write full blog posts for me is substantially higher. So let’s do it.
Formatting Data for AI Fine-tuning
The first step in the process is to get our data and convert it to a format that can be used to fine-tune our model. For WordPress, the WP CLI is a fast, effective way to do this. We’ll use it to query my posts and convert them to JSONL, which is really a fancy way of saying this is JSON with one record per line.
To do this, I’ve used the following bash script, which allows me to create two JSONL files for fine-tuning. I ran this in my localhost after syncing my live site to include all articles; I recommend you do the same.
if (isset($_GET['export']) && in_array($_GET['export'], ['train', 'valid'])) {
global $wpdb;
$posts = $wpdb->get_results("
SELECT
p.post_title,
p.post_date,
p.post_content_filtered,
pm.meta_value as lead_in
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = 'extras_lead-in'
WHERE p.post_status = 'publish'
AND p.post_type = 'post'
AND p.post_content_filtered != ''
ORDER BY p.post_date DESC
");
$total = count($posts);
$train_count = floor($total * 0.8);
if ($_GET['export'] === 'train') {
$filtered_posts = array_slice($posts, 0, $train_count);
$filename = 'train.jsonl';
} else {
$filtered_posts = array_slice($posts, $train_count);
$filename = 'valid.jsonl';
}
header('Content-Type: application/json');
header("Content-Disposition: attachment; filename=\"{$filename}\"");
foreach ($filtered_posts as $post) {
$date = date('F j, Y', strtotime($post->post_date));
$title_with_lead = $post->post_title . (!empty($post->lead_in) ? ' - ' . $post->lead_in : '');
$jsonl = json_encode([
'messages' => [
[
'role' => 'user',
'content' => "write a blog post in markdown for kevinleary.net entitled \"{$title_with_lead}\" as if it were originally published on {$date}",
],
[
'role' => 'assistant',
'content' => $post->post_content_filtered,
],
],
], JSON_UNESCAPED_UNICODE);
echo $jsonl . "\n";
}
exit;
}
Once this is in place, we can generate the two files we need for fine-tuning by visiting your site with the following URL parameters added to the URL:
- ?export=train
- ?export=valid
Each will prompt you to save a .jsonl file to your system, which becomes our training data for fine-tuning. In this case, it formats my WordPress posts, which are written in markdown, into the following JSON data structure:
{
"messages": [
{
"role": "user",
"content": "write a blog post in markdown for kevinleary.net entitled '{TITLE} - {TEASER / LEAD-IN}' as if it were originally published on August 10, 2025"
},
{
"role": "assistant",
"content": "{MARKDOWN CONTENT}"
}
]
}
We’ll use this as our training data in the fine-tuning process.
Setting Up the Training Environment
To train our AI model, we’ll use a framework specifically built for Apple Silicon called MLX, which runs on Python, so we’ll need to install that if you don’t already have it. Using the package installer provided by Python is the best way I’ve found.
Once you’ve installed Python, install MLX with pip:
pip install mlx-lm
The first step is to download a local copy of the model we’ll be training, then feed our JSONL to it to create our custom model. I’ve saved mine into a ~/AI/ directory on my Mac.
Running the Fine-tuning Process
We can use it to start the fine-tuning process with a single command:
mlx_lm.lora --model mlx-community/Meta-Llama-3.1-8B-Instruct-4bit --data . --train --batch-size 1 --num-layers 16 --iters 600 --learning-rate 1e-5 --save-every 100
When the training process completes, we’re left with an ./adaptors directory containing adaptor files. These are the files that we can use to create a CLI or UI to work with our model.
Using the Custom LLM
Once we have our completed adaptors as output from the training process, we can use the model in the command line:
mlx_lm.generate --model mlx-community/Meta-Llama-3.1-8B-Instruct-4bit --adapter-path adapters.npz --prompt "Write a blog post for kevinleary.net about JavaScript performance"
That’s a mouthful; it’s best to simplify with a custom CLI function. I use Fish shell, so I’ve created a helpful fish function to make it better:
Fish CLI Function
function kevinlearynet-writer
mlx_lm.generate \
--model mlx-community/Meta-Llama-3.1-8B-Instruct-4bit \
--adapter-path /Users/kevinleary/AI/adapters/ \
--prompt $argv \
--max-tokens 4000 \
--temp 0.8 \
--top-p 0.9 \
--min-p 0.05
end
Bash Function
If you use Bash as a shell you can add this to your .bash_profile and run the same command to generate content.
function kevinlearynet-writer() {
mlx_lm.generate \
--model mlx-community/Meta-Llama-3.1-8B-Instruct-4bit \
--adapter-path /Users/kevinleary/AI/adapters/ \
--prompt "$*" \
--max-tokens 4000 \
--temp 0.8 \
--top-p 0.9 \
--min-p 0.05
}
Now I can use my custom AI model in the Terminal whenever I want like this:
kevinlearynet-writer "Write a blog post for kevinleary.net about JavaScript performance"
Conclusion
You should have a pretty good idea of how to create a custom LLM by fine-tuning an open-source model from huggingface.com. In this example, we’re creating a writing assistant that can match my voice and writing style by training on blog posts from Kevin Leary.net. If you’re a business that has done content marketing and has a fair amount of content written, you can do the same, and if you’re using WordPress, you can use the exact same methods for pulling the data out, but the same process could be used with any CMS, just using different methods.
This is only one example of how to use a custom LLM to improve your workflow over out-of-the-box tools like ChatGPT and Claude, but the general process can be used for many different applications. You provide data, fine-tune with training, and then you get a more specialized LLM that does what you want with more accuracy and less hallucination.
Creating a custom fine-tuned LLM provides a superior approach to working with AI for specialized situations such as debugging code in your company’s specific framework, generating marketing copy that matches your brand voice, or providing technical support answers using your product’s exact API documentation and error messages.
If you’re looking for a good AI consultant to help with training custom models, give me a shout.
@dealignai Pretty sure there is solid evidence that prefill is indeed quite a bit faster for 256gb+ models. See GLM 5.1 benchmarks earlier this year on similar M3 configs: mlx#3402. See comment by @angeloskath. Happy to be proven wrong, as I’m interested in this myself.
4 likes · 300 views
Some recent awesome optimizations merged in MLX:
Built-in sliding window support in fused SDPA
mlx#4734
Use mma/nax for QMV with small rows and large N/K, making batched decoding 2~3x faster
mlx#4712
Dequantize fp4 into float16 directly, which makes prefilling 1.4x faster for old macs
mlx#4709
Add a 2-stage nax QMM kernel that makes small prompt prefill almost 2x faster for some models
mlx#4682
1-bit affine quantization
mlx#3161
Thin GEMM kernels making small-M/large-N and large-M/small-N 1.5x faster
mlx#4640
1.4x faster fused SDPA by prefetching K/V
mlx#4596
And there are actually a lot more awesome changes that I'm too exhausted to list them all, the commit history of MLX is an interesting read for anyone who wants to make open source contributions. github.com/ml-explore/mlx/commits/main
48 likes · 2 reposts · 2,463 views
Квантование RWKV-7: что ломается в моделях разного размера
1. Введение
Модели семейства RWKV с линейной по длине последовательности рекуррентной структурой привлекательны для локального запуска, но инструментов квантования для них заметно меньше, чем для трансформеров. В этой работе я описываю инструмент rwkv-quant, сжимающий RWKV-7 двумя готовыми пресетами, и систематически разбираю, где именно при квантовании теряется качество и как это зависит от размера модели.
Работа началась с наблюдения, которое противоречит обычной интуиции «чем больше модель, тем меньше относительная потеря». Пресет COMPRESSION на 2.9B показал +4.04% по перплексии против +2.85% на 1.5B. Выяснение причины заняло основную часть исследования и привело к набору результатов, которые я считаю полезными независимо от конкретного инструмента.
Вклад работы.
Локализация потерь. Разложение KL по группам матриц (leave-one-out) показывает, что на 2.9B вред сосредоточен в двух местах: выходной проекции нулевого блока (o_proj слоя 0) и таблице эмбеддинга. Хранение первой в bf16 стоит 0.5% файла и снимает 39% KL; перевод второй с 5 на 6 бит стоит 1.1% файла и снимает ещё 22% (раздел 4).
Механизм и предсказуемость. Вход проблемной матрицы несёт массивный канал, но сама концентрация энергии вред не предсказывает: у 7.2B она выражена сильнее всех, а модель квантуется лучше всех. Я показываю, что три естественные локальные метрики ошибки не работают, и описываю метрику, которая работает: ошибка в остаточном потоке, умноженная на измеренное усиление (раздел 5).
Автоподбор битности по правилу «вред на байт», работающий в обе стороны (одна цена байта для повышения и понижения), с измерением по одной матрице за проход (раздел 6).
GPTQ на сетке пресета без изменения формата, ядер и размера файла; показано, что регуляризация важнее размера калибровки и что методы автоподбора и GPTQ складываются почти мультипликативно (раздел 7). GPTQ в пресете REDUCTION описан в разделе 8.
Сводка по шести размерам от 0.1B до 13.3B с доверительными интервалами, полученными парным бутстрэпом по окнам (раздел 9), и сравнение с GGUF и MLX при равном размере файла (раздел 10).
Методологические находки. Утечка калибровочного корпуса в оценочный набор и её измеренный масштаб (раздел 3.4); разброс результата GPTQ между численно эквивалентными реализациями и его количественная оценка (раздел 7.6); четыре инженерные ошибки, исказившие измерения (приложение A).
2. Связанные работы
Квантование с коррекцией ошибки. GPTQ (Frantar et al., 2022) квантует колонки матрицы по одной и разносит ошибку каждой на ещё не квантованные колонки через обратный гессиан входных активаций. Я переношу GPTQ на асимметричную суперблочную сетку (раздел 7) и симметричную сетку типа Q6_K (раздел 8). Формат файла и ядра при этом не меняются.
Взвешивание по активациям. AWQ и матрица важности imatrix в llama.cpp используют статистики активаций, чтобы защитить более важные каналы. В моём инструменте статистика активаций (AW, activation-weighted) определяет выбор масштаба и сдвига групп весов; без неё 4 бита в o_proj разрушают 2.9B (раздел 4.3).
Выбросы и массивные активации. Dettmers et al. (2022, LLM.int8()) описали выбросы признаков, концентрированные в нескольких каналах скрытого состояния, и их роль в деградации квантованных трансформеров. Sun et al. (2024) описали «массивные активации» — значения на порядки больше медианных в немногих каналах. Массивный канал на входе o_proj нулевого блока (раздел 4.3) — прямая родня этих явлений, но, как я покажу, его сила сама по себе не предсказывает вред.
3. Постановка и методика
3.1 Модели
Исследуется линейка RWKV-7 шести размеров. Для изоляции факторов использованы три пары: 0.4B/1.5B (одна глубина, ширина вдвое больше), 2.9B/7.2B (одна глубина L=32, ширина 2560 против 4096) и 7.2B/13.3B (одна ширина, глубина 32 против 61).
модель
слоёв L
ширина D
голов H=D/64
D/L
серии чекпоинтов
0.1B
12
768
12
64
g1d
0.4B
24
1024
16
42.7
g1d
1.5B
24
2048
32
85.3
g1d, g1h, g1j
2.9B
32
2560
40
80
g1h, g1j
7.2B
32
4096
64
128
g1j
13.3B
61
4096
64
67.2
g1j
Итоговые таблицы (разделы 9–10) относятся к серии g1j; диагностика (раздел 4) частично использует g1d и g1h, это указано в тексте.
3.2 Пресеты
Инструмент rwkv-quant сжимает модель двумя пресетами.
REDUCTION (почти без потерь). Проекции (proj) — 8 бит, симметричная раскладка sym_aw с группой 16 весов; cmix (каналовое микширование, FFN) — 6 бит; эмбеддинг и голова — 8 бит; LoRA-ветки w, a, v — 6 бит, g — 8 бит (блок 64). Сжатие около 2.1x.
COMPRESSION (агрессивный). Проекции — 4 бита, асимметричная суперблочная раскладка asym_sb6_aw с группой 32; cmix — 4 бита, но ffn.key — 5 бит; эмбеддинг — 6 бит (в ранних измерениях 5, раздел 4.5); голова — 5 бит; LoRA-ветки те же, что в REDUCTION. Сжатие около 3.1x.
В раскладках с суффиксом _aw масштаб и сдвиг каждой группы подбираются поиском, взвешенным по среднему квадрату входной активации канала E[x²], то есть по статистике, снятой на калибровочном корпусе. Раскладка REDUCTION симметричная (масштаб на блок из 16 весов, без сдвига; int8-коды масштабов против одного fp16-множителя на суперблок из 16 блоков; 6 или 8 бит). Раскладка COMPRESSION (sb6) асимметричная суперблочная: блок из 32 весов, суперблок из 8 блоков (256 колонок); масштаб и сдвиг каждого блока хранятся 6-битными кодами qs и qm относительно двух fp16-множителей суперблока.
Не квантуются и хранятся в bf16 малые поканальные векторы (k_k, k_a, r_k), нормы и bias LoRA-веток. У таблицы эмбеддинга статистики активаций нет (её вход — индексы токенов), поэтому её сетка в обоих пресетах подбирается без AW-взвешивания; GPTQ эмбеддинг также не затрагивает.
Две правки, найденные в разделе 4, входят в финальный COMPRESSION: выходная проекция нулевого слоя (o_proj слоя 0) хранится в bf16, эмбеддинг — в 6 битах. Пресет без этих правок в тексте называется «базовым» (в логах compression_nol0).
Рис. 1. Конвейер quantize(): от чекпоинта bf16 до файла .rwkvq
Рис. 1. Конвейер quantize(). Статистика активаций E[x²] используется автоподбором, GPTQ и записью; результат автоподбора дописывается в конфиг (bits_overrides). Калибровка не зависит от платформы; формат и бэкенд отделены от неё.
Рис. 2. Битность каждой матрицы блока RWKV-7 в двух пресетах
Рис. 2. Битность матриц в пресетах REDUCTION и COMPRESSION. Выделены две фиксированные правки: o_proj слоя 0 в bf16 и эмбеддинг на 6 бит вместо 5 (раздел 4). Автоподбор (раздел 6) меняет биты отдельных матриц поверх пресета COMPRESSION.
3.3 Метрики и приборы
Основная метрика — KL(bf16‖квант) на токен: расхождение распределения следующего токена у квантованной модели от распределения bf16-модели. Эталонные логиты считаются в fp32 и сохраняются на диск (5.09 ГБ на модель). Вторая метрика — относительное изменение перплексии (Δppl, в процентах) к bf16-модели. Эталон принимается числом (оценка эталонного плеча), а не размером файла.
Используются два пути вычислений, и их абсолютные KL различаются, поэтому сравнивать можно только внутри одного прибора:
fake-путь (PyTorch): веса квантуются и тут же деквантуются, вычисления в плотных тензорах; работает на Mac (MPS) и на сервере (CUDA);
реальный путь (MLX): коды упакованы в формат .rwkvq и читаются Metal-ядрами; только Mac.
Приборы перечислены в приложении C.
3.4 Оценочные наборы и утечка
Первый набор eval_corpus_multiling (en/ru/sr, 38 окон по 512 токенов) использовался в разделах 4–6 и оказался целиком лежащим внутри калибровочного корпуса пакета: общих 16-грамм 100%. Для метода, калибруемого по корпусу, это прямая утечка.
Масштаб утечки измерен на GPTQ (раздел 7), откалиброванном по корпусу пакета: на 1.5B выигрыш по KL текста составил −61.8% на старом наборе и −27.1% на чистом, то есть утечка завысила выигрыш в 2.3 раза. Код чист (одно окно из 24 содержит общие 16-граммы, 0.03%).
AW-статистика задета частично: её 23 окна по 512 токенов содержат 23.9% 16-грамм старого набора и пересекаются с 13 из 38 его окон. Абсолютные числа раздела 4 получены на тексте, по которому AW частично взвешивала поиск; направление смещения в пользу AW, величина не измерялась. Сравнения внутри раздела (какая матрица, какой бит) сделаны при одной и той же статистике, и смещение входит в оба плеча. Автоподбор утечкой не воспользовался: на 1.5B −16.1% на старом наборе и −17.6% на чистом.
Чистые наборы (ни одной общей 16-граммы ни с одним калибровочным корпусом):
eval_text_heldout: Википедия en/ru/sr, по 12 окон на язык (36 окон);
eval_code_heldout: 24 окна Python/Swift (закрытый набор);
eval_code_open: 48 окон (24 Python, 24 Swift) из открытых репозиториев автора.
Правило: любой калиброванный метод мерится только на чистых наборах, и перед замером проверяется, что пересечение 16-грамм калибровки с набором равно нулю. Разделы 4–5 и таблица выбора порога в разделе 6.3 измерены на старом наборе (в таблице раздела 6.3 окна не участвовали в выборе, но лежат внутри калибровочного корпуса); результат автоподбора перепроверен на чистых наборах в разделе 6.4. Разделы 7–10 используют только чистые наборы.
3.5 Статистика
Доверительные интервалы строятся парным бутстрэпом по окнам (20 000 повторов, окна ресэмплируются вместе для обоих сравниваемых плеч). При сравнении с чужими форматами то же самое делается для отношений KL. У KL полуширина 95%-интервала составляет 7–12% от значения. Дополнительно, для оценки разброса самого GPTQ между реализациями, использованы реплики с переставленным порядком окон калибровки (раздел 7.6).
4. Где теряется качество
Замечание о наборах. Измерения разделов 4 и 5 сделаны на первом оценочном наборе (раздел 3.4), частично пересекающемся с AW-статистикой. Абсолютные значения могут быть слегка смещены в пользу AW; сравнения между матрицами и битностями внутри раздела сделаны при одной статистике. Все дальнейшие результаты (разделы 6.4–10) получены на чистых наборах уже с найденными здесь правками в пресете.
Базовый вопрос: почему COMPRESSION на 2.9B деградирует сильнее, чем на 1.5B (+4.04% против +2.85% по ppl)?
4.1 Разложение потерь по группам матриц
Вклад группы матриц определяется методом leave-one-out: вклад = KL(всё квантовано) − KL(всё, кроме этой группы). Результаты для серии g1h, реальный путь (Mac, MLX):
группа
1.5B: KL (доля)
1.5B: ppl, п.п.
2.9B: KL (доля)
2.9B: ppl, п.п.
o_proj
0.00578 (16%)
0.64
0.02368 (46%)
1.82
cmix.value
0.01317 (37%)
0.99
0.01035 (20%)
0.63
эмбеддинг
0.00120 (3%)
0.07
0.00874 (17%)
0.83
cmix.key
0.00442 (12%)
0.43
0.00400 (8%)
0.36
голова
0.00440 (12%)
0.39
0.00310 (6%)
0.36
att.value
0.00443 (12%)
0.37
0.00378 (7%)
0.52
r, k, LoRA
≤4%
≤0.08
≤2%
≤0.12
Сумма вкладов равна 101% и 107%, то есть группы почти аддитивны. На 2.9B выделяются две группы: o_proj (46% всего KL) и эмбеддинг (17%); на 1.5B их вклад скромный (16% и 3%), главный вклад даёт cmix.value.
Тот же разбор на сервере (fake-путь, серия g1j, базовый пресет compression_nol0) подтверждает картину:
модель
базовый KL
o_proj слоя 0
весь o_proj
эмбеддинг
cmix.value
голова
1.5B
0.050962
0.5%
13%
2.8%
29%
8.3%
2.9B
0.077513
39%
45%
30%
—
—
7.2B
0.022984
3.9%
13%
8.4%
25%
9.4%
Рис. 3. Доли потерь KL по группам матриц на 1.5B и 2.9B
Рис. 3. Доли потерь KL по группам матриц (leave-one-out, реальный путь, серия g1h), 1.5B и 2.9B. Данные первой таблицы раздела.
4.2 Главная находка: o_proj нулевого слоя
На 2.9B 85–86% вклада всей группы o_proj приходится на одну матрицу: выходную проекцию нулевого блока. Прямой эксперимент (реальный путь, 2.9B, g1h) сравнивает базовый пресет с его вариантами, в которых отдельные части o_proj хранятся с большей точностью:
плечо
KL
ppl
en / ru / sr
прирост файла
базовый
0.052048
+4.04%
+4.24 / +3.21 / +5.71
—
o_proj слоя 0 в bf16
0.031966
+2.43%
+3.02 / +2.19 / +2.36
+9.4 МБ (+0.5%)
o_proj слоя 31 в bf16
0.051512
+4.07%
≈ базовый
+9.4 МБ
весь o_proj в 5 бит
0.046043
+3.54%
ru хуже: 3.21→3.44
+26.2 МБ
весь o_proj в 6 бит
0.036305
+2.89%
—
+52.4 МБ
Одна матрица 2560×2560 в bf16 возвращает 85% вклада группы o_proj за 0.5% файла и работает лучше, чем перевод всего o_proj в 6 бит за 2.8% файла. Контрольное плечо (слой 31) эффекта не даёт, то есть дело именно в нулевом слое. После правки 2.9B становится лучше 1.5B: +2.43% против +2.85%.
Та же правка на других масштабах (реальный путь):
пресет
модель
KL: базовый → с правкой
ppl
прирост файла
COMPRESSION
0.1B
0.08960 → 0.08279 (−7.6%)
+8.68% → +8.01%
+0.8 МБ
COMPRESSION
0.4B
0.04965 → 0.04891 (−1.5%)
+4.31% → +4.28%
+1.5 МБ
COMPRESSION
1.5B
0.03599 → 0.03585 (−0.4%)
+2.85% → +2.87%
+6.0 МБ
COMPRESSION
2.9B
0.05205 → 0.03197 (−39%)
+4.04% → +2.43%
+9.4 МБ
REDUCTION
0.4B / 1.5B
без изменений
—
+1.0 / +3.9 МБ
REDUCTION
2.9B
0.001635 → 0.001573 (−3.8%)
+0.08% → +0.06%
+6.1 МБ
Значимы только эффекты на 0.1B и 2.9B: на 0.4B и 1.5B изменение кросс-энтропии не отличается от нуля. Поправка существенна лишь в COMPRESSION на 2.9B и, в меньшей степени, на 0.1B.
4.3 Механизм: массивный канал на входе o_proj слоя 0
Вход o_proj нулевого слоя несёт «массивный канал»: один-два канала на несколько порядков сильнее остальных по среднему квадрату активации E[x²].
модель
max/медиана E[x²]
доля энергии в 8 каналах
номер канала
0.1B g1d
2 628
0.77
112
0.4B g1d
22 580
0.91
53
1.5B g1d
2 245
0.46
1202
1.5B g1h
3 740
0.53
1202
1.5B g1j
4 227
0.54
1202
2.9B g1h
7 199 995
0.98
517
2.9B g1j
7 564 462
0.98
517
7.2B g1j
6 626 554
0.99
160
Номер канала один и тот же у всех серий одной ширины: 1202 у трёх серий 1.5B, 517 у двух серий 2.9B. Следовательно, это свойство ширины и архитектуры, а не конкретного прогона обучения.
Насколько AW-статистика держит эту матрицу, видно из плеча «o4s»: те же 4 бита, но поиск без AW, только в o_proj.
модель
KL: базовый → без AW
ppl
1.5B
0.035992 → 0.042974
+2.85% → +3.57%
2.9B
0.052048 → 0.340025
+4.04% → +37.2%
На 2.9B AW держит почти всё: без неё 4 бита в o_proj разрушают модель.
4.4 Почему именно 2.9B, а не «все большие модели»
Ожидание «больше модель — меньше деградация» в целом верно: на 2.9B все обычные группы (r, k, v, cmix, голова, LoRA) уносят меньше, чем на 1.5B, даже в абсолютных числах. Выбиваются только o_proj и эмбеддинг.
Однако на 7.2B массивный канал выражен сильнее всех (99% энергии в восьми каналах), а квантуется эта модель лучше всех: базовый KL 0.023 против 0.078 у 2.9B и 0.051 у 1.5B (одна серия g1j, один прибор), и правка слоя 0 даёт там лишь −3.9%. Значит, сила массивного канала сама по себе не объясняет вред.
Прямой замер вреда от квантования одной матрицы (квантуется только o_proj слоя 0, остальное в bf16) на 2.9B и 7.2B:
KL
‖Δx‖/‖x‖ на слое 0
пик по глубине
2.9B
0.020290
13%
31% (слои 13–16)
7.2B
0.001427
3.4%
6.7% (слой 13)
Разница в 14 раз по KL создаётся в момент впрыска ошибки в остаточный поток, а не при её распространении: усиление вниз по глубине у обеих моделей близкое (2.4x и 2.0x к середине сети). Это подводит к вопросу, чем вред вообще предсказывается (раздел 5).
Рис. 4. Сила массивного канала и вред от квантования o_proj слоя 0
Рис. 4. Сила массивного канала (max/медиана E[x²]; серии g1j) и вред от квантования только o_proj слоя 0 на 2.9B и 7.2B. Сила канала почти одинакова, вред различается в 14 раз.
4.5 Второй адрес: эмбеддинг
На 2.9B эмбеддинг уносит 17% KL (серия g1h) и 30% (g1j), на 1.5B — 3%. Перевод эмбеддинга из 5 бит в 6 (реальный путь, поверх пресета с правкой слоя 0):
модель
KL
ppl
en / ru / sr
прирост файла
2.9B, базовый
0.031966
+2.43%
+3.02 / +2.19 / +2.36
—
2.9B, эмбеддинг в 6 бит
0.024885 (−22%)
+1.86%
+2.33 / +1.66 / +1.82
+21.0 МБ (+1.13%)
2.9B, эмбеддинг в bf16
0.024539 (−23%)
+1.82%
—
+220 МБ
1.5B, эмбеддинг в 6 бит
0.034933 (−2.6%)
+2.82%
шум
+16.8 МБ
6 бит забирают 95% того, что даёт полный bf16, в десять раз дешевле. Восемь бит для эмбеддинга недоступны: суперблочная раскладка знает только 4, 5 и 6 бит. Обе правки вместе переводят 2.9B с +4.04% до +1.86% ценой примерно 1.6% файла.
Решение: эмбеддинг в 6 бит входит в COMPRESSION всегда. Пересчёт по записанным файлам (реальный путь, 38 окон, прежний файл меряется тем же прибором рядом):
модель
ppl: было → стало
KL: было → стало
ΔCE, 95% ДИ
прирост файла
0.1B
+8.00% → +7.50%
0.0828 → 0.0780
значимо
+6.3 МБ (+4.9%)
0.4B
+4.28% → +4.20%
0.0489 → 0.0468
в шуме
+8.4 МБ (+2.9%)
1.5B
+2.87% → +2.82%
0.0359 → 0.0349
в шуме
+16.8 МБ (+1.7%)
2.9B
+2.43% → +1.85%
0.0320 → 0.0249
значимо
+21.0 МБ (+1.1%)
Цена правки — только байты файла: в COMPRESSION эмбеддинг разворачивается в плотный fp16 (gather применим лишь к симметричной раскладке), и на каждый токен читается одна строка таблицы.
4.6 Скорость
Скорость декода 2.9B измерена на двух файлах, собранных одним кодом и отличающихся только слоем 0 (bf16 против квантованной матрицы), с чередованием ABBA в одном процессе:
порядок загрузки
разница
файл без правки первым
−0.40 мс/ток (−1.6%)
файл с правкой первым
−0.52 мс/ток (−2.2%)
контроль A/A
−0.02 [−0.13; +0.10]
Файл с bf16-матрицей не медленнее, а быстрее, хотя читает на 9.4 МБ больше. Механизм не выяснен: одна матрица не должна давать полмиллисекунды, и я не утверждаю объяснения. Вывод, который из этого следует практически: правка слоя 0 не стоит скорости.
5. Что предсказывает вред от квантования матрицы
Раздел 4 показывает, что вред от квантования очень неравномерен между матрицами и что одна матрица с «массивным» входом может быть важнее целой группы. Возникает вопрос, можно ли предсказать такие матрицы заранее, не квантуя каждую по отдельности. Я проверил три естественных признака и все три отвергли.
Признак 1: концентрация энергии входа. Доля энергии в восьми каналах не предсказывает вред: 0.99 у 7.2B (вреда практически нет), 0.77 у 0.1B (вред есть), 0.91 у 0.4B (вреда нет).
Признак 2: диагональная ошибка выхода слоя
У нулевого слоя она наименьшая во всех пяти проверенных моделях. Причина понятна: AW-поиск минимизирует именно эту величину, то есть признак измеряет качество поиска, а не вред.
Признак 3: честная локальная ошибка впрыска ‖(W−Ŵ)X‖/‖WX‖ на настоящих активациях. На 2.9B максимум у ffn.value слоёв 15–19 (0.16), а o_proj слоя 0 не попадает даже в первую дюжину. Контроль: квантование только blocks.15.ffn.value даёт KL 0.000776 против 0.020290 у o_proj слоя 0, то есть в 26 раз меньше вреда при вчетверо большей локальной ошибке.
Вывод. Локальная ошибка нормирована не на то. Значима ошибка, внесённая в остаточный поток, умноженная на то, насколько последующие слои её усиливают. След ошибки слоя 15 затухает по глубине (4.9% → 2.2%), а след слоя 0 разгоняется (13% → 31%).
Признак, который работает. Ошибка подслоя в остаточном потоке ‖Δx‖²/‖x‖², умноженная на усиление G этой точки, где G измерено прямо: шумом ε = 1% в ту же точку и измерением KL на выходе. Ранговая корреляция с истинным одиночным вредом (KL при квантовании только этой матрицы): 0.977 (0.1B), 0.969 (1.5B), 0.920 (2.9B); верхние три матрицы на 2.9B совпадают. Слабости признака: вклад эмбеддинга через точку x₀ переоценён (изотропный шум не похож на настоящую ошибку), а внутренние матрицы слоя 0 недооценены (v_first не моделируется).
Для практического правила этот признак оказался не нужен. Правило платит за байт (раздел 6), и для него честнее всего прямой замер одиночного вреда каждой матрицы: один проход на матрицу, что по цене сопоставимо с измерением усиления G (2L+1 проходов).
6. Автоподбор битности: правило «вред на байт»
Фиксированный список правок («o_proj слоя 0 в bf16, эмбеддинг в 6 бит») плох тем, что проблемные матрицы у разных моделей разные. Ниже описано правило выбора битности по измерению, работающее в обе стороны.
6.1 Правило
Для матрицы m и шага её битности b → b′ задана величина
где KL берётся для модели, у которой квантована только эта матрица, против fp32-эталона. Шаг вверх выполняется, если e ≥ τ; шаг вниз — если e < τ. Один порог τ с обеих сторон означает одну цену байта: биты переезжают туда, где окупаются сильнее.
Стоимость матрицы в байтах считается как params·(b + 0.5)/8 (0.5 бита на вес — масштабы sb6) и 2·params для bf16. Лестница уровней — 4, 5, 6 бит и bf16. Шаг вниз допустим только до 4 бит (3-битной раскладки в формате нет); эмбеддинг вниз не идёт никогда. Подъёмы выбираются жадно по убыванию e, пока e ≥ τ. Для заданного бюджета файла (по умолчанию +0.5%) берётся наименьшее τ, при котором прирост байт укладывается в бюджет; оно ищется бисекцией по log τ. Если бюджет недостижим даже при самом большом τ, возвращается выбор при этом τ и пометка «недостижим».
Цель выбора — средний KL по 14 окнам калибровочного корпуса, взятым по языковой квоте en 4 / code 4 / ru 2 / sr 2 / zh 2 (веса языков пропорциональны числу окон; при необходимости веса можно задать явно).
6.2 Кандидаты и понижение
Первая версия ошибалась в выборе кандидатов, а не в признаке. Первоначально проверялись 12 верхних матриц по абсолютному вреду плюс концевые слои. Но правило платит за байт, а мелкие матрицы C×C (att.output и att.value средних и поздних слоёв) при умеренном вреде вчетверо дешевле cmix. Строгая граница снимает вопрос без предположений: шаг вверх не может снять больше одиночного вреда S матрицы, значит
По свипу одиночного вреда за пределами 12 кандидатов матриц с границей ≥ 5 оказалось 47 (1.5B), 61 (2.9B) и 59 (7.2B). Полный выбор при τ = 5 — 33 / 46 / 45 ключей вместо 12.
Вниз три бита не нужны. Лестница вниз (KL одной матрицы на бит ниже) показала, что шаги на 3 бита при e < 5 дают не более 0.9% файла (7.2B), обычно 0.2–0.6%. Дешёвые байты лежат в другом месте: ffn.key 5 → 4 в средних слоях (e = 1.9–3.5), а 4-битная раскладка в формате уже есть. Новая раскладка не нужна.
6.3 Выбор порога τ
Симметричное правило оказывается байт-нейтральным при τ ≈ 5–6: размер пресета был верным, неверным было распределение бит. Ниже — KL файла на 30 окнах, не участвовавших в выборе (но лежащих внутри калибровочного корпуса, раздел 3.4), парно к пресету по окнам; скорость — чередование ABBA в одном процессе на Mac.
Δ размера
ΔKL, отложенные окна
окон хуже
Δ декода
1.5B, прежний выбор (12 ключей, только вверх)
+22.6 МБ
−17.0% [−18.2; −15.8]
0/30
+1.7%
1.5B, sym τ=5
+6.8 МБ
−18.8% [−20.3; −17.4]
0/30
в шуме
1.5B, sym τ=6
−2.6 МБ
−13.5% [−15.1; −11.7]
1/30
−0.5%
1.5B, sym τ=7
−24.6 МБ
−0.8% [−2.8; +1.2]
13/30
−2.0%
2.9B, прежний выбор
+55.4 МБ
−24.6% [−26.1; −23.1]
0/30
+2.4%
2.9B, sym τ=5
+16.9 МБ
−25.7% [−28.4; −22.5]
0/30
в шуме
2.9B, sym τ=6
−8.5 МБ
−19.2% [−21.6; −16.6]
0/30
в шуме
2.9B, sym τ=7
−49.2 МБ
−6.5% [−9.0; −3.8]
6/30
−2.4%
7.2B, sym τ=5
+86.0 МБ
−25.8% [−28.4; −23.6]
0/30
не мерено
7.2B, sym τ=6
−33.6 МБ
−13.4% [−16.6; −10.2]
0/30
не мерено
7.2B, sym τ=7
−115.3 МБ
−0.7% [−4.3; +2.8]
12/30
не мерено
τ = 5 даёт качество прежнего выбора за треть байт и без потери скорости; τ = 7 — качество пресета на файле на 2.5% меньше и на 2% быстрее. Скорость здесь не выводится из байт: прежний выбор добавлял 22.6 МБ и замедлял на 1.7%, а sym τ=5 добавляет 6.8 МБ и не замедляет, потому что байты сместились с матриц, читаемых на каждом токене, на другие.
Оговорка, которую данные сделали сами. В этом прототипе выбор делался по 8 окнам (3 ru / 3 en / 2 sr), и английский выигрывает меньше всех (7.2B, τ=6: en −7.7% против sr −19.4%; при τ=7 английский даже на 4.6% хуже пресета). То, что правило минимизирует, — это решение о целевых данных, а не деталь реализации.
Рис. 5. Порог τ: размер файла и снятый вред на отложенных окнах
Рис. 5. Выбор порога τ: изменение размера файла и KL на 30 отложенных окнах (парно к пресету, 95% ДИ). Данные таблицы выше.
6.4 Чистая оценка на моделях от 1.5B до 13.3B
Прибор: fake-путь на сервере, 36 окон чистого текста и 24 окна чистого кода, KL к bf16 тем же прибором (для 1.5B — замер на Mac). Бюджет +0.5% файла. Изменение относительно пресета без автоподбора:
модель
KL текст
en
ru
sr
KL код
Δppl текст
Δppl код
1.5B (Mac)
−17.6%
−17.9
−16.9
−18.0
−13.5%
+3.88 → +3.25%
+3.86 → +3.32%
7.2B
−20.7%
−25.1
−16.1
−20.3
−13.6%
+1.85 → +1.51%
+1.22 → +0.86%
13.3B
−21.1%
−23.4
−17.0
−22.5
−17.4%
+1.14 → +0.86%
+1.11 → +0.58%
С ростом модели выигрыш не падает. Предсказание самого измерения на 13.3B завышено в 1.5 раза: предсказано −32.9%, измерено −21.1% (на 1.5B предсказание было близким). Поэтому, как и выше, результатом считается только замер записанного файла.
Полная таблица с парным бутстрэпом для шести моделей приведена в приложении B.1. Её итог: по KL автоподбор значимо лучше пресета на каждой модели и в каждом срезе; по ppl выигрыш не значим в двух клетках (текст на 0.4B и код на 2.9B). Слабое место — английский на 0.4B: KL −7.5%, три окна из двенадцати хуже пресета, ppl +0.46% [−0.25; +1.14].
6.5 Что выбирает автоподбор
Число матриц, у которых бит повышен или понижен (бюджет +0.5% файла):
модель
вверх
вниз
что вниз
0.1B
47
1
—
0.4B
52
10
—
1.5B
46
16
—
2.9B
59
25
ffn.key 5 → 4 бита
13.3B
110
37
—
Наверх идут в основном o_proj и v_proj (5 бит) и ffn.value. Бюджет +0.5% не выбирается целиком: правило упирается в τ, и прирост файла составляет 0.1–0.2% (на 0.4B размер даже уменьшается).
6.6 Измерение без лестниц и его цена
Уровни выше битности пресета измерять не нужно: KL одной матрицы убывает с битом почти одинаково у всех типов матриц и масштабов. Отношение r = KL(b+1)/KL(b) составляет 0.315 (proj), 0.313 (cmix.value), 0.317 (cmix.key) и 0.41 (голова), медианы по 500+ шагам на шести моделях от 0.1B до 13.3B. Выбор по формуле «измеренная база × rᵏ» даёт тот же КПД (процент снятого KL на мегабайт), что выбор по истинным лестницам, на всех проверенных масштабах. Предсказанный выигрыш при этом завышен (1.5B, τ=6: предсказано −19%, измерено −13%), поэтому отчёт правила служит ориентиром.
Цена измерения — один проход на матрицу на битности пресета и один на реализуемый шаг вниз. Проход возобновляется с чистого остаточного потока на входе слоя матрицы: слои до неё квантование не меняет, результат побитно тот же, что у полного прохода (проверено гейтом), а работы в среднем вдвое меньше. Прибор — fp32-модель с весами, хранящимися в bf16: апкаст точен, поэтому это та же fp32-модель при вдвое меньшей памяти; эталон считается тем же прибором в том же процессе, и пол KL равен нулю по построению.
6.7 Что правило не делает
Правило не спасает от отсутствия AW-статистики (раздел 4.3: без неё 2.9B разрушается) и не отменяет фиксированных правок пресета: o_proj слоя 0 и эмбеддинг в 6 бит остаются его стартовой точкой. На больших моделях измерение считается там, где хватает памяти, и переносится как файл со строгой подписью чекпоинта — так же, как imatrix для GGUF.
7. GPTQ на сетке пресета COMPRESSION
Замечание. Таблицы разделов 7.2–7.5 и приложения B получены серверным прототипом GPTQ. Итоговые числа по файлам, которые пишет библиотека, приведены в разделе 9; точечные значения прототипа и библиотеки различаются в пределах разброса реализаций (раздел 7.6). Все измерения этого раздела — на чистых наборах (раздел 3.4).
7.1 Метод
Я переношу GPTQ (Frantar et al., 2022) на асимметричную суперблочную сетку asym_sb6_aw:
масштаб и сдвиг суперблока (256 колонок) вычисляются в его начале, по уже поправленным весам, тем же AW-поиском, что используется при обычной записи (RTN);
колонки квантуются по одной;
ошибка каждой колонки разносится на ещё не квантованные через H⁻¹.
Проход последовательный: вход каждой матрицы снимается с модели, в которой предыдущие матрицы уже квантованы (порядок: r, k, v → o → ffn.key → ffn.value, голова в конце). Эмбеддинг — таблица поиска, GPTQ его не затрагивает. Формат файла, ядра и размер не меняются.
Проверки корректности: при диагональной H коды побитно равны RTN (проверка 0); файл, записанный библиотекой, побитно равен fake-пути GPTQ (гейт).
7.2 Регуляризация важнее размера калибровки
Первая версия: 48 окон корпуса пакета, демпфирование damp = 0.01. KL к RTN, чистый набор:
модель
текст
en
код
2.9B
−21.1%
−12.2
−37.9%
7.2B
+0.4%
+13.6
−21.4%
13.3B
+4.7%
+17.7
−21.4%
На больших моделях английский текст стал хуже RTN. Причина — переобучение: 24.5 тыс. токенов на оценку H размером до 16384×16384. Это подтверждается двумя контрольными прогонами на 7.2B: damp 0.1 на тех же 48 окнах даёт −21.0% (en −17.1), а 82 окна при damp 0.01 дают лишь −4.9% (en +6.7). Дело именно в регуляризации, а не в числе окон.
7.3 Калибровочный корпус v2
600 окон по 512 токенов (307 тыс. токенов), собраны из открытых источников:
доля
состав
en 30%
FineWeb-Edu
ru 20%
FineWeb-2, русская литература, oasst2
код 20%
UltraData-Code, 6 языков, плюс открытый код проекта
zh 10%
FineWeb-2, novel_text
sr 10%
кириллица и латиница пополам
рассуждения 10%
дистилляции двух моделей
Порядок окон — взвешенный круг между группами, поэтому любой префикс сохраняет доли. Окна, содержащие 16-граммы любого оценочного набора, выброшены.
7.4 Результаты: GPTQ против RTN
KL к RTN-пресету при damp 0.1 на чистых наборах (текст / en / ru / sr / код) и изменение Δppl:
модель
окон
KL
Δppl текст
Δppl код
0.1B
150
−33.6 / −34.7 / −30.6 / −35.8 / −35.8
+7.80 → +5.12%
—
1.5B
150
−39.6 / −41.3 / −39.8 / −37.7 / −46.7
+3.91 → +2.63%
+3.84 → +2.18%
1.5B
600
−42.2 / −40.2 / −42.5 / −43.9 / −49.9
+3.91 → +2.55%
+3.84 → +1.85%
2.9B
150
−38.7 / −37.5 / −38.4 / −40.1 / −43.1
+2.78 → +1.69%
+2.33 → +1.53%
2.9B
600
−42.5 / −42.2 / −42.6 / −42.7 / −46.4
+2.78 → +1.49%
+2.33 → +1.04%
7.2B
150
−22.8 / −18.6 / −22.4 / −27.5 / −34.2
+1.85 → +1.44%
+1.22 → +0.84%
7.2B
600
−28.8 / −25.4 / −27.2 / −33.8 / −38.5
+1.85 → +1.31%
+1.22 → +0.76%
13.3B
150
−21.0 / −17.3 / −19.9 / −26.4 / −37.3
+1.14 → +1.13%
+1.11 → +0.45%
13.3B
600
−28.8 / −26.4 / −27.8 / −32.6 / −38.5
+1.14 → +0.86%
+1.11 → +0.69%
Ни один язык ни на одной модели не хуже RTN. damp 0.1 оказался лучше 0.01 и на малых моделях: на 1.5B и 2.9B при 150 окнах разница составляет 5–7 п. Большим моделям нужно больше окон: переход 150 → 600 даёт +6–8 п. по тексту на 7.2B и 13.3B и +3–4 п. на 1.5B и 2.9B. Английский выигрывает меньше других языков на всех масштабах.
Парный бутстрэп по окнам (600 окон калибровки; приложение B.2) показывает, что GPTQ значимо лучше RTN по KL и по ppl на каждой модели, по тексту и по коду; хуже RTN по KL оказалось одно окно из 360 (7.2B, текст).
7.5 Вместе с автоподбором
Измеряется KL к пресету без обоих методов, 600 окон калибровки. «Произведение» — ожидание при независимости методов: (1 − выигрыш GPTQ) × (1 − выигрыш автоподбора).
модель
KL текст
произведение
KL код
Δppl текст
Δppl код
0.1B
−62.3%
−59.8%
−63.1%
+7.80 → +2.84%
+6.83 → +2.68%
0.4B
−45.6%
−47.9%
−42.8%
+4.43 → +2.72%
+4.74 → +2.22%
1.5B
−52.0%
−52.3%
−55.1%
+3.91 → +2.11%
+3.84 → +1.49%
2.9B
−45.2%
−53.4%
−51.4%
+2.78 → +1.28%
+2.33 → +1.04%
7.2B
−42.5%
−43.5%
−44.9%
+1.85 → +1.07%
+1.22 → +0.46%
13.3B
−41.3%
−43.9%
−47.6%
+1.14 → +0.58%
+1.10 → +0.27%
На пяти моделях из шести методы складываются почти мультипликативно, в пределах 3 п. Исключение — 2.9B: поверх GPTQ автоподбор добавляет лишь −4.7% по тексту (−9.2% по коду) вместо ожидаемых около −19%. Первая гипотеза (автоподбор на 2.9B тратит байты на o_proj слоя 0, а GPTQ уже чинит эту матрицу) оказалась неверной: o_proj слоя 0 хранится в bf16 в обоих пресетах и автоподбор её не трогает. Объяснение одними понижениями бит тоже неубедительно: доля понижений на 2.9B наибольшая, но на 13.3B лишь немного меньше, а сложение там почти мультипликативно. Затем выяснилось, что аномалия — артефакт пары прогонов прототипа (раздел 7.6): по файлам библиотеки автоподбор поверх GPTQ на 2.9B даёт −11.5% [−14.7; −8.2] и значим.
Важная деталь: измерение автоподбора делается при RTN. Прямая проверка зависимости — снять измерение по модели, в которой остальные матрицы уже прошли GPTQ, и сравнить выбор; она не проведена (раздел 11).
7.6 Воспроизводимость и разброс GPTQ
Детерминизм. Библиотечный GPTQ детерминирован: два прогона на разных картах и файл, собранный quantize() под нагрузкой сервера, побитно совпали (0.4B и 1.5B, REDUCTION, 144 тензора из 144).
Разброс между реализациями. Две численно эквивалентные реализации дают разные коды. Прототип брал верхний множитель Холецкого, библиотека — нижний с транспонированием; разница порядка 1e-15, но GPTQ разносит её по всей матрице. Итог файла библиотеки против прототипа, тем же прибором:
ΔKL, текст / код
Δppl код
1.5B COMPRESSION
+1.1 / +4.2%
+1.83 против +1.49%
2.9B COMPRESSION
−5.3 / +0.6%
—
0.4B COMPRESSION
+0.2 / +0.1%
—
Знак случайный. Значит, у каждого одиночного GPTQ-числа есть погрешность «реализации» порядка ±5% KL и до ±0.35 п. ppl. Сравнения внутри этого разброса не доказаны.
Реплики. Чтобы измерить разброс, а не оценивать его по трём парам, библиотечный GPTQ повторён с переставленным порядком 600 окон калибровки: окна те же, меняется только порядок суммирования H, то есть возмущение того же рода, что смена множителя Холецкого. Файлы пишет quantize(), оценка — тот же прибор по записанному файлу; код здесь — открытый набор (48 окон). Умолчание COMPRESSION: 0.4B — 6 реплик, 1.5B и 2.9B — по 4; «только GPTQ» на 2.9B — 4 реплики.
модель, плечо
KL текст: sd / размах
KL код: sd / размах
размах Δppl текст / код, п.
0.4B, умолчание
0.6% / 1.6%
0.8% / 2.2%
0.38 / 0.37
1.5B, умолчание
1.3% / 2.8%
1.4% / 2.9%
0.19 / 0.15
2.9B, умолчание
1.2% / 2.7%
2.1% / 4.7%
0.27 / 0.19
2.9B, только GPTQ
0.6% / 1.5%
1.3% / 2.9%
0.28 / 0.13
Что из этого следует.
Погрешность одиночного GPTQ-числа внутри библиотеки — около ±1.5% KL (одно sd), размах до 3–5%. Это меньше прежней оценки ±5%, сравнивавшей библиотеку с прототипом. Исходный порядок окон из облака реплик не выделяется (отклонение от среднего по перестановкам в пределах ±2%).
По ppl разброс реплик сравним с интервалом по окнам: размах 0.15–0.4 п. при Δppl +1…+2.7%. Различия Δppl между плечами меньше 0.3 п. по одному файлу не доказаны.
Шум отдельного окна — 4–8% KL (медиана sd по репликам). Поэтому счёт «в скольких окнах хуже» при эффектах порядка 10% ненадёжен, а среднее по окнам устойчиво.
Автоподбор поверх GPTQ на 2.9B значим. Среднее по репликам, интервал — совместный бутстрэп по окнам и репликам: текст −11.5% [−14.7; −8.2], код −7.4% [−10.6; −3.8]; все 16 пар реплик дают выигрыш (текст от −12.8 до −9.2%). На 0.4B и 1.5B (одна реплика «только GPTQ») выигрыш по тексту −12…−13% и −12…−14%.
Методы складываются чуть хуже произведения, на 2.9B сильнее всего: отношение измеренного к произведению — 2.9B текст 1.09 [1.05; 1.13], код 1.11 [1.07; 1.16]; 0.4B 1.03–1.05; 1.5B 1.04–1.07 по тексту и около 1.0 по коду. Прежние 1.18 на 2.9B сложились из двух отклонений прототипа в разные стороны: «только GPTQ» у прототипа 0.01560 против 0.01601 у библиотеки (среднее реплик), умолчание — 0.01486 против 0.01417.
7.7 Стоимость
Время GPTQ на RTX 4090 при 600 окнах:
модель
карт
время
1.5B
1
42 мин
2.9B
1
81 мин
7.2B
2
2.6 ч
13.3B
4
4.7 ч
Активации калибровки держатся на хосте (2 × окна × 511 × D × 4 байта), слои разнесены по картам.
На MacBook Air M4 16 ГБ без вентилятора, пресет REDUCTION, вызов quantize() целиком:
модель
время
пик памяти
своп (изменение за прогон)
0.1B
17 мин
7.7 ГБ
+0
0.4B
49 мин
8.9 ГБ
+0
1.5B
2 ч 26 мин
19.5 ГБ
−1.4 ГБ
На 1.5B пик превышает объём ОЗУ: macOS сжимает память, время на слой остаётся ровным (около 6 мин). Оценка «модель + активации», по которой библиотека решает, запускать ли GPTQ неявно, занижает пик в 2.4–3.4 раза. Файлы, собранные на Mac (цикл GPTQ на CPU), по реальному пути совпадают с серверными в пределах разброса реализаций (1.5B, Δppl по двум наборам: +0.20 / +0.06% против +0.27 / +0.04%).
8. GPTQ в пресете REDUCTION
REDUCTION хранит проекции, cmix и голову в симметричной раскладке типа Q6_K (раздел 3.2). GPTQ переносится на эту сетку так же, как на асимметричную: сетка суперблока строится в его начале по поправленным весам, дальше идёт тот же цикл. Проверка 0 при диагональной H — побитное равенство RTN на 6 и 8 битах. Калибровка — те же 600 окон, damp 0.1.
KL к RTN-REDUCTION на чистых наборах (текст / en / ru / sr / код) и Δppl. Время — на одной RTX 4090 при совместной загрузке карт другими прогонами, поэтому ориентировочное:
модель
KL
Δppl текст
Δppl код
время
0.1B
−22.4 / −28.7 / −19.5 / −20.6 / −27.9
+0.50 → +0.47%
+0.60 → +0.48%
14 мин
0.4B
−22.7 / −21.5 / −22.7 / −23.7 / −27.2
+0.29 → +0.21%
+0.23 → +0.18%
29 мин
1.5B
−28.4 / −24.5 / −29.1 / −31.4 / −32.1
+0.35 → +0.27%
+0.07 → +0.05%
43 мин
2.9B
−29.6 / −26.2 / −31.2 / −31.4 / −33.6
+0.15 → +0.09%
+0.15 → +0.15%
75 мин
7.2B
−21.1 / −12.6 / −23.6 / −27.3 / −24.3
+0.12 → +0.13%
+0.09 → +0.09%
2.4 ч, 2 карты
13.3B
−17.1 / −16.2 / −20.0 / −15.2 / −31.6
+0.07 → +0.07%
+0.10 → +0.09%
5.2 ч, 4 карты*
- Время на 4 картах при совместной загрузке сервера другими прогонами.
Ни один язык ни на одной модели не хуже RTN по KL. Относительный выигрыш (−17…−30% по тексту) меньше, чем в COMPRESSION (−34…−42%): для 8 бит это ожидаемо, ошибка округления мала и компенсировать нечего. С размером модели выигрыш не растёт: на 7.2B и 13.3B он наименьший по тексту, а английский на 7.2B выигрывает всего −12.6%. По коду выигрыш держится на всех масштабах (−24…−34%).
Парный бутстрэп по окнам (приложение B.4) подтверждает, что выигрыш по KL значим везде, на каждом языке каждой модели; самая слабая граница — английский на 7.2B, верхний край интервала −4.8%. По окнам GPTQ хуже RTN по KL не более чем в одном окне из 36 (текст) или 24 (код).
Изменение ppl не различимо ни на одной модели: все интервалы накрывают ноль, их ширина ±0.1–0.15%, что сравнимо с самой деградацией REDUCTION (+0.1–0.5%). По знаку ppl чаще лучше: у четырёх моделей из шести точечная оценка по тексту отрицательна, у 7.2B и 13.3B — +0.01%. Два языка, где ppl наклоняется в худшую сторону:
английский на 7.2B: +0.10% [−0.04; +0.24], P(хуже) = 0.92 — не значимо;
русский на 13.3B: +0.11% [+0.03; +0.19], P(хуже) = 0.997 — значимо; KL этого же языка при этом −20.0% [−24.8; −15.6].
Это около 30 парных сравнений (6 моделей × 5 срезов) на уровне 95%. Одно ложное срабатывание в такой серии ожидаемо, поэтому единичный значимый случай не опровергает вывод, но он записан.
Решение: GPTQ включён по умолчанию и в REDUCTION. Довод: REDUCTION служит базой для QLoRA и векторных моделей (эмбеддеров), где важна верность скрытых состояний исходной модели, а не ppl. Сетка в библиотеке та же, упаковка общая с RTN; RTN-файл после рефакторинга побайтно прежний. Библиотечный GPTQ на 0.1B воспроизводит прототип точно: KL и CE совпадают до шестого знака.
Вывод. GPTQ в REDUCTION значимо приближает модель к bf16 по распределению (KL), но на этом наборе не даёт измеримого выигрыша по ppl. Для near-lossless пресета это выигрыш в верности исходной модели, а не в качестве на задаче; чтобы увидеть его по ppl, нужно на порядок больше окон. Байты при этом не меняются.
9. Итоговые результаты по шести размерам
Здесь шесть масштабов измерены так, как результат получит пользователь: файлы пишет quantize() библиотеки, оценка идёт по записанному файлу одним прибором на чистых наборах (36 окон текста и 48 окон открытого кода). Интервалы Δppl — парный бутстрэп по окнам, 95%. Файл GPTQ имеет тот же размер, что RTN-файл той же конфигурации. Автоподбор — бюджет +0.5% файла; GPTQ — 600 окон калибровки v2, damp 0.1. Жирным выделено умолчание библиотеки (quantize(preset="compression") включает автоподбор и GPTQ).
Размеры файлов (МБ и доля от bf16):
модель
bf16
REDUCTION
COMPRESSION + автоподбор
0.1B
382
191 (50.1%)
134 (35.2%)
0.4B
902
434 (48.1%)
299 (33.2%)
1.5B
3055
1439 (47.1%)
997 (32.6%)
2.9B
5896
2743 (46.5%)
1892 (32.1%)
7.2B
14400
6648 (46.2%)
4576 (31.8%)
13.3B
26541
12178 (45.9%)
8377 (31.6%)
Качество:
модель
плечо
МБ
KL текст
KL код
Δppl текст, %
Δppl код, %
0.1B
REDUCTION
191.3
0.00550
0.00555
+0.49 [+0.33; +0.65]
+0.75 [+0.60; +0.90]
0.1B
REDUCTION + GPTQ (умолчание)
191.3
0.00426
0.00399
+0.46 [+0.33; +0.59]
+0.55 [+0.41; +0.69]
0.1B
COMPRESSION
133.9
0.07420
0.06944
+7.76 [+6.81; +8.73]
+6.61 [+5.84; +7.43]
0.1B
- автоподбор
134.4
0.04524
0.04011
+5.13 [+4.59; +5.66]
+3.56 [+3.13; +3.98]
0.1B
- GPTQ
133.9
0.04838
0.04082
+4.95 [+4.33; +5.58]
+3.60 [+3.11; +4.09]
0.1B
- оба (умолчание)
134.4
0.02864
0.02476
+2.76 [+2.28; +3.25]
+2.47 [+2.18; +2.77]
0.4B
REDUCTION
434.1
0.00313
0.00280
+0.28 [+0.16; +0.40]
+0.21 [+0.11; +0.32]
0.4B
REDUCTION + GPTQ (умолчание)
434.1
0.00242
0.00205
+0.20 [+0.10; +0.31]
+0.19 [+0.11; +0.28]
0.4B
COMPRESSION
301.2
0.04805
0.04264
+4.40 [+3.75; +5.07]
+4.38 [+3.85; +4.90]
0.4B
- автоподбор
299.2
0.04038
0.03757
+4.19 [+3.67; +4.72]
+3.56 [+3.07; +4.07]
0.4B
- GPTQ
301.2
0.02956
0.02564
+2.96 [+2.44; +3.48]
+2.45 [+2.02; +2.91]
0.4B
- оба (умолчание)
299.2
0.02599
0.02306
+2.57 [+2.11; +3.04]
+2.00 [+1.60; +2.42]
1.5B
REDUCTION
1439.0
0.00239
0.00244
+0.35 [+0.22; +0.47]
+0.13 [+0.02; +0.23]
1.5B
REDUCTION + GPTQ (умолчание)
1439.0
0.00171
0.00162
+0.27 [+0.17; +0.37]
+0.03 [-0.04; +0.10]
1.5B
COMPRESSION
993.4
0.03779
0.04061
+3.90 [+3.40; +4.43]
+3.85 [+3.40; +4.30]
1.5B
- автоподбор
996.5
0.03119
0.03597
+3.29 [+2.86; +3.72]
+3.59 [+3.10; +4.08]
1.5B
- GPTQ
993.4
0.02135
0.02073
+2.54 [+2.13; +2.94]
+1.76 [+1.44; +2.09]
1.5B
- оба (умолчание)
996.5
0.01831
0.01874
+1.98 [+1.55; +2.44]
+1.69 [+1.28; +2.13]
2.9B
REDUCTION
2743.3
0.00177
0.00180
+0.15 [+0.05; +0.26]
+0.05 [-0.04; +0.14]
2.9B
REDUCTION + GPTQ (умолчание)
2743.3
0.00125
0.00120
+0.09 [-0.01; +0.19]
+0.05 [-0.02; +0.12]
2.9B
COMPRESSION
1885.5
0.02708
0.02976
+2.77 [+2.33; +3.22]
+2.63 [+2.21; +3.03]
2.9B
- автоподбор
1891.8
0.02192
0.02480
+2.12 [+1.65; +2.58]
+2.18 [+1.77; +2.59]
2.9B
- GPTQ
1885.5
0.01610
0.01514
+1.75 [+1.42; +2.08]
+1.30 [+1.02; +1.58]
2.9B
- оба (умолчание)
1891.8
0.01406
0.01394
+1.32 [+1.09; +1.56]
+1.05 [+0.76; +1.34]
7.2B
REDUCTION
6647.8
0.00119
0.00112
+0.12 [+0.04; +0.21]
+0.11 [+0.04; +0.18]
7.2B
REDUCTION + GPTQ (умолчание)
6647.8
0.00093
0.00081
+0.13 [+0.07; +0.20]
+0.08 [+0.02; +0.14]
7.2B
COMPRESSION
4555.0
0.01682
0.01686
+1.88 [+1.56; +2.20]
+1.51 [+1.23; +1.79]
7.2B
- автоподбор
4576.0
0.01331
0.01437
+1.52 [+1.17; +1.87]
+1.09 [+0.80; +1.40]
7.2B
- GPTQ
4555.0
0.01201
0.00978
+1.02 [+0.68; +1.37]
+0.71 [+0.49; +0.94]
7.2B
- оба (умолчание)
4576.0
0.00992
0.00857
+0.95 [+0.68; +1.22]
+0.67 [+0.48; +0.86]
13.3B
REDUCTION
12178.4
0.00082
0.00090
+0.07 [+0.00; +0.13]
+0.11 [+0.05; +0.16]
13.3B
REDUCTION + GPTQ (умолчание)
12178.4
0.00068
0.00065
+0.07 [+0.02; +0.13]
+0.10 [+0.05; +0.15]
13.3B
COMPRESSION
8337.1
0.01128
0.01228
+1.15 [+0.87; +1.44]
+1.12 [+0.85; +1.39]
13.3B
- автоподбор
8377.0
0.00889
0.01020
+0.88 [+0.64; +1.12]
+0.75 [+0.53; +0.96]
13.3B
- GPTQ
8337.1
0.00814
0.00705
+0.83 [+0.59; +1.08]
+0.38 [+0.21; +0.56]
13.3B
- оба (умолчание)
8377.0
0.00687
0.00615
+0.70 [+0.49; +0.93]
+0.40 [+0.23; +0.57]
Что видно.
Относительная деградация монотонно падает с размером модели в обоих пресетах. COMPRESSION, KL текста: 0.074 → 0.048 → 0.038 → 0.027 → 0.017 → 0.011. REDUCTION: 0.0055 → 0.0031 → 0.0024 → 0.0018 → 0.0012 → 0.0008. Аномалия 2.9B из раздела 4 после правок пресета (o_proj слоя 0, эмбеддинг в 6 бит) в этой линейке уже не видна. Потери умолчания COMPRESSION падают с размером и по Δppl: +2.76, +2.57, +1.98, +1.32, +0.95, +0.70% по тексту.
Сжатие растёт с размером. Доля «неконтролируемых» тензоров (LoRA-ветки, малые тензоры, нормы) убывает: REDUCTION 50% → 46%, COMPRESSION 35% → 31.4%.
Оба метода окупаются сильнее всего на самой малой модели. На 0.1B умолчание снимает 62% KL пресета, на остальных 41–52% (раздел 7.5). Поэтому умолчание COMPRESSION на 0.4B (+2.57%) лучше голого пресета на 1.5B (+3.90%), а умолчание на 2.9B (+1.32%) — голого пресета на 7.2B (+1.88%).
Автоподбор почти не стоит байт. Прибавка составляет 0.1–0.2% файла; на 0.4B размер даже уменьшился. Бюджет +0.5% не выбирается целиком: правило упирается в τ.
О строгости монотонности. Для голых пресетов монотонность по размеру держится с запасом: интервалы KL соседних масштабов не пересекаются ни в REDUCTION, ни в COMPRESSION (по таблице прототипа). Для умолчания COMPRESSION она держится по точечным оценкам, но интервалы соседних масштабов пересекаются (например, KL текста 0.0279 [0.0263; 0.0296] у 0.1B против 0.0260 [0.0242; 0.0278] у 0.4B — по предварительной таблице прототипа — и аналогично у пары 1.5B/2.9B). Интервалы Δppl соседних масштабов в основном перекрываются; строго различимы далёкие масштабы, например 0.1B (+2.76 [+2.28; +3.25]) и 13.3B (+0.70 [+0.49; +0.93]).
Замена закрытого набора кода на открытый ни одного вывода не сдвинула: KL по двум наборам кода расходится в пределах 10%.
Рис. 6. Потери ppl по шести размерам модели
Рис. 6. Δppl относительно bf16 для шести размеров: RTN, автоподбор, GPTQ и их сочетание. Данные из таблицы итогов этого раздела.
10. Сравнение с GGUF и MLX при равном размере файла
Чужие форматы собраны из того же чекпоинта g1j и измерены тем же прибором, что мои файлы: их квантованные веса деквантуются и подставляются в эталонную модель, дальше — те же окна и та же KL к bf16.
GGUF: llama-quantize, Q4_K_M и Q6_K с imatrix по тем же 600 окнам, что у моего GPTQ; деквантование — gguf-py. В этой сборке Q4_K_M держит голову в Q6_K, а LoRA — в F16.
MLX: схема опубликованных файлов (affine, группа 64). int6 — всё в 6 бит; int4 — проекции и FFN в 4 бита, эмбеддинг, голова и LoRA в 6. Коды снимает сам mx.quantize, деквантование на сервере побитно равно mx.dequantize.
Мои файлы равного размера: сетка REDUCTION (симметричная, тип Q6_K) с 6 битами. sym6 — проекции, эмбеддинг и голова в 6 бит (размер int6); sym6h8 — только проекции в 6 бит, эмбеддинг и голова в 8 (размер Q6_K). Автоподбора здесь нет: это не оптимизированные точки, а простейшая замена битности.
1.5B
формат
МБ
KL текст
KL код
Δppl текст, %
Δppl код, %
MLX int4
970.4
0.11642
0.12479
+12.71 [+11.43; +14.03]
+12.14 [+11.14; +13.16]
GGUF Q4_K_M (imatrix)
990.1
0.03926
0.04049
+4.07 [+3.48; +4.72]
+3.76 [+3.32; +4.21]
мой COMPRESSION (умолчание)
996.5
0.01831
0.01874
+1.98 [+1.54; +2.44]
+1.69 [+1.28; +2.12]
мой sym6, без GPTQ
1272.3
0.00375
0.00402
+0.51 [+0.35; +0.67]
+0.29 [+0.15; +0.43]
мой sym6 + GPTQ
1272.3
0.00260
0.00251
+0.31 [+0.17; +0.45]
+0.16 [+0.04; +0.27]
MLX int6
1272.4
0.00901
0.00971
+0.83 [+0.64; +1.03]
+0.54 [+0.34; +0.75]
GGUF Q6_K (imatrix)
1336.1
0.00406
0.00426
+0.48 [+0.34; +0.61]
+0.27 [+0.15; +0.39]
мой sym6h8, без GPTQ
1339.4
0.00304
0.00329
+0.41 [+0.27; +0.54]
+0.21 [+0.07; +0.33]
мой sym6h8 + GPTQ
1339.4
0.00208
0.00207
+0.27 [+0.16; +0.38]
+0.11 [+0.01; +0.22]
мой REDUCTION (умолчание)
1439.0
0.00171
0.00162
+0.27 [+0.17; +0.37]
+0.03 [-0.04; +0.10]
2.9B
формат
МБ
KL текст
KL код
Δppl текст, %
Δppl код, %
MLX int4
1824.2
0.50748
0.46978
+66.14 [+53.11; +81.85]
+55.45 [+47.30; +64.44]
GGUF Q4_K_M (imatrix)
1875.8
1.37847
1.50555
+304.99 [+203.92; +497.81]
+328.67 [+201.28; +647.98]
мой COMPRESSION (умолчание)
1891.8
0.01406
0.01394
+1.32 [+1.09; +1.56]
+1.05 [+0.76; +1.34]
мой sym6, без GPTQ
2451.4
0.00288
0.00305
+0.27 [+0.12; +0.42]
+0.16 [+0.05; +0.26]
мой sym6 + GPTQ
2451.4
0.00205
0.00206
+0.22 [+0.10; +0.34]
+0.05 [-0.04; +0.15]
MLX int6
2453.4
0.01514
0.01967
+1.16 [+0.75; +1.54]
+1.67 [+1.33; +2.02]
мой sym6h8, без GPTQ
2535.3
0.00223
0.00239
+0.22 [+0.11; +0.34]
+0.15 [+0.05; +0.25]
мой sym6h8 + GPTQ
2535.3
0.00155
0.00153
+0.20 [+0.10; +0.31]
-0.01 [-0.11; +0.08]
GGUF Q6_K (imatrix)
2567.9
0.00512
0.00714
+0.33 [+0.11; +0.53]
+0.63 [+0.42; +0.85]
мой REDUCTION (умолчание)
2743.3
0.00125
0.00120
+0.09 [-0.01; +0.19]
+0.05 [-0.02; +0.12]
Отношения KL «чужой формат / мой файл» (парный бутстрэп по окнам; значение > 1 означает, что мой файл точнее). В скобках — разница размера моего файла относительно чужого.
1.5B
чужой / мой
размер моего
KL текст
KL код
GGUF Q4_K_M (imatrix) / мой COMPRESSION (умолчание)
+0.7%
2.14 [1.98; 2.30]
2.16 [2.05; 2.28]
MLX int4 / мой COMPRESSION (умолчание)
+2.7%
6.36 [5.84; 6.92]
6.66 [6.20; 7.14]
MLX int6 / мой sym6 + GPTQ
-0.0%
3.47 [3.23; 3.74]
3.86 [3.72; 4.01]
MLX int6 / мой sym6, без GPTQ
-0.0%
2.40 [2.29; 2.53]
2.42 [2.35; 2.49]
GGUF Q6_K (imatrix) / мой sym6h8 + GPTQ
+0.2%
1.95 [1.82; 2.09]
2.06 [1.95; 2.17]
GGUF Q6_K (imatrix) / мой sym6h8, без GPTQ
+0.2%
1.34 [1.27; 1.40]
1.29 [1.24; 1.35]
MLX int6 / мой COMPRESSION (умолчание)
-21.7%
0.49 [0.45; 0.53]
0.52 [0.48; 0.56]
GGUF Q6_K (imatrix) / мой REDUCTION (умолчание)
+7.7%
2.37 [2.18; 2.58]
2.63 [2.49; 2.76]
MLX int6 / мой REDUCTION (умолчание)
+13.1%
5.27 [4.88; 5.71]
5.98 [5.70; 6.28]
2.9B
чужой / мой
размер моего
KL текст
KL код
GGUF Q4_K_M (imatrix) / мой COMPRESSION (умолчание)
+0.9%
98.06 [75.72; 129.80]
108.00 [81.83; 149.56]
MLX int4 / мой COMPRESSION (умолчание)
+3.7%
36.10 [29.93; 42.99]
33.70 [29.72; 38.19]
MLX int6 / мой sym6 + GPTQ
-0.1%
7.38 [6.57; 8.42]
9.57 [8.71; 10.57]
MLX int6 / мой sym6, без GPTQ
-0.1%
5.26 [4.72; 5.95]
6.46 [5.93; 7.05]
GGUF Q6_K (imatrix) / мой sym6h8 + GPTQ
-1.3%
3.31 [2.92; 3.76]
4.65 [4.25; 5.09]
GGUF Q6_K (imatrix) / мой sym6h8, без GPTQ
-1.3%
2.30 [2.08; 2.56]
2.99 [2.75; 3.25]
MLX int6 / мой COMPRESSION (умолчание)
-22.9%
1.08 [0.94; 1.26]
1.41 [1.27; 1.57]
GGUF Q6_K (imatrix) / мой REDUCTION (умолчание)
+6.8%
4.10 [3.63; 4.69]
5.97 [5.45; 6.51]
MLX int6 / мой REDUCTION (умолчание)
+11.8%
12.13 [10.62; 14.15]
16.45 [14.99; 18.18]
Что из этого следует.
При равном размере
MacBook Air(M5)でローカルLLM環境を構築してみた
はじめに
MacBook Air(M5、メモリ24GB)にローカルLLM環境を構築して「とりあえず動かしてみる」という記事です。
結論として、今回はgemma4を使ったのですが、軽量なモデルでも思ったより回答までに時間がかかるなという印象です。
(普段ClaudeCodeを使っているので、期待値が高すぎたのかも。)
とはいえ、Claudeがリミットになったので一時的にローカルLLMで代用したいときはありなのかなと思います。
機密情報を扱うのでオフライン環境でLLMを使いたいなどの明確な目的・理由がある場合には、素直にパソコンのスペックを上げましょう。
!
本記事はあくまでも「使ってみた」的な実験記事です。ローカルLLMの利用目的があるわけでもなく、どれくらい動くのか試してみたい、くらいの記事です。主観・体感をまとめたものとして読んでください。気になる人は自分でやってみてください。意外と簡単に環境構築できます。
PCスペック
私のMacbook Airは以下の通りです。
4月頃にIntel Macから買い替えたのですが、なんで24GBにしたのかは覚えていません。。
項目
スペック
機種
MacBook Air
チップ
Apple M5
メモリ
24GB(ユニファイドメモリ)
OS
macOS 27.0(Golden Gate)
ローカルLLM実行環境
ローカルLLM環境にはいくつか種類がありますが、今回は手軽さを優先して以下の2つにしています。
opencode:ターミナルで動くオープンソースのAIコーディングエージェント。ClaudeやCodexの他にローカルで動作するモデルも使える。
Ollama:ローカルでLLMを動かすためのツール。CLIでモデルのダウンロード(ollama pull)から起動まで完結し、127.0.0.1 でAPIサーバーとしても動作する。今回はopencodeからこのAPI利用する。(Ollama単体でもターミナルからチャットできる。)
VS Code:コードエディタ。opencodeの拡張機能を導入して、VScode上でopencodeを使う。
Ollama構築
CLIとAPIサーバーだけで足りるので、Homebrewでインストールします。
公式サイトからダウンロードでも大丈夫です。
brew install ollama
インストール後、Ollamaを起動。ターミナルを閉じるとOllamaも終了します。
ollama serve
Ollamaを自動起動したい場合
ログイン時に自動実行したい場合は、以下でhomebrewで管理してくれます。
Ollama公式からGUIアプリをインストールすると、アプリでログイン時の動作も管理できるみたいです。(試してません。)
brew services start ollama
状態確認と停止は以下の通りです。
brew services list # 状態確認
brew services stop ollama
モデルをダウンロード
ollamaで動かせるモデルはここからダウンロードできます。
自分の端末のスペックをAIに投げて、おすすめの(動作しそうな)モデルを聞いてみてください。
今回は、GoogleがGeminiと同じ技術をもとに開発した軽量モデル「gemma」を使います。
24GBだと量子化版(ざっくり言うと情報量を落とした軽量版らしい)が現実的みたい。
なので、以下をとりあえずダウンロード(コマンドはpull)してみる。軽いモデルでも数GBはあるので、色々試した後はollama listで一覧を確認して、ollama rm <モデル名>で削除できます。
ollama pull gemma4:e2b
ollama pull gemma4:e4b
ollama pull gemma4:e2b-mlx
ollama pull gemma4:e4b-mlx
ollama pull gemma4:12b
ollama show gemma4:e2bで表示されるquantizationの項目を見ると、量子化方式を確認できます。gemma4:e2bはQ4_K_M(4bit量子化)でした。
mlxとは
タグ名の-mlxは、Apple製の機械学習フレームワークMLX向けに作られたモデルを指します。MLXはApple Siliconの、CPUとGPUで同じメモリを共有するユニファイドメモリを活かせるように設計されています。
ollama show gemma4:e2b-mlxで確認すると、量子化方式はnvfp4でした。gemma4:e2b(Q4_K_M)と量子化の形式が違うので、速さや回答の違いを比べてみると面白いかもしれません。
ちなみに、gemma3(gemma3n:e2b、gemma3n:e4b、gemma3:4b-it-qat)もダウンロードしましたが、opencodeからの呼び出しには対応していませんでした。
opencode構築
インストール
opencodeもHomebrewでインストールできます。opencodeのデスクトップアプリもあるみたいですが今回はVScodeから使うので、brew install opencodeでインストールします。
brew install opencode
opencode --version
ターミナルから使う場合は、プロジェクトフォルダまで移動して、opencodeで実行できます。
cd ~/your/project/
opencode
ollamaへの接続設定
opencodeの設定ファイル~/.config/opencode/opencode.jsonにollamaへの接続を定義します。
ollamaはOpenAI互換のAPI(127.0.0.1
modelsのキーには、ollama listに表示されるモデル名をそのまま書きます。以下はこの記事でpullしたモデルを定義した例です。opencodeで使いたいモデルを増やしたいときには、ollama pullしたあとに追記します。
~/.config/opencode/opencode.json
{
"$schema": "opencode.ai",
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"name": "Ollama",
"options": {
"baseURL": "127.0.0.1"
},
"models": {
"gemma4:12b": {
"name": "gemma4:12b"
},
"gemma4:e2b": {
"name": "gemma4:e2b"
},
"gemma4:e4b": {
"name": "gemma4:e4b"
},
"gemma4:e2b-mlx": {
"name": "gemma4:e2b-mlx"
},
"gemma4:e4b-mlx": {
"name": "gemma4:e4b-mlx"
}
}
}
}
}
VSCode構築
VSCode側は環境構築というほどでもなく、拡張機能でopencodeを入れるだけ。エディタ上からopencodeを呼び出せるようになります。
設定は上記のopencode.jsonを共通で参照するので、拡張機能側で別途モデルを設定する必要はありません。
使った感想
このMacBook Airでは、Pythonのhelloを出力するだけでもClaude Codeなどの有料プランより遅く感じました。普段からClaudeを使っている人には、物足りなく感じると思います。
どんな用途に向いていそうか
ネットに繋がらない・繋げたくない環境で、簡単なコード生成や質問応答を試したいとき
クラウドAIが使えなくなったときの、応急的な作業継続
スペック次第でこの辺りの体感は変わってくるはずなので、気になる人はまず自分の環境で試してみてください。
Muse Spark Open: 816B parámetros abiertos de Meta
Meta acaba de liberar los pesos de Muse Spark Open, un modelo de 816.000 millones de parámetros que corre completo en dos Mac Studio con chip M5 Ultra, sin pasar por ningún servidor en la nube.
Es la versión de pesos abiertos del sistema que Meta Superintelligence Labs presentó semanas atrás, y llega con una ventana de contexto de 1 millón de tokens pensada para ingeniería de software de varios pasos y trabajo agéntico prolongado.
TL;DR
- Meta publicó los pesos de Muse Spark Open, modelo MoE de 816.000 millones de parámetros con 42.000 millones activos.- La ventana de contexto llega a 1 millón de tokens, pensada para tareas de ingeniería de software largas.- Dos Mac Studio con chip M5 Ultra y MLX corren el modelo completo gracias a 512 GB de memoria unificada por equipo.- Los kernels Helion en SGLang aceleran la inferencia entre 1,03 y 3,95 veces en GPU Nvidia y AMD.- El modelo está disponible en Hugging Face y vía APIs de Together AI, Fireworks, Baseten, Groq, Modal, Nebius y OpenRouter.
Qué es Muse Spark Open
Muse Spark Open es el modelo de pesos abiertos derivado de Muse Spark, el sistema multimodal de mezcla de expertos de Meta con 816.000 millones de parámetros totales, 42.000 millones activos por token y una ventana de contexto de 1 millón de tokens, pensado para programación y trabajo agéntico de varios pasos.
El modelo es nativamente multimodal: procesa texto e imágenes dentro de la misma arquitectura de mezcla de expertos (MoE), y activa solo 42.000 millones de los 816.000 millones de parámetros en cada token. Esa activación parcial es lo que distingue a un MoE de un modelo denso, porque el costo de cómputo por token se parece al de un modelo mucho más chico, aunque la memoria necesaria para guardar todos los expertos sigue siendo la de un modelo de 816.000 millones.
Qué pasó
El modelo de pesos abiertos de Meta ya está disponible para descargar en Hugging Face, Amazon SageMaker AI, Google Cloud Model Garden y Microsoft Foundry Models. Para quien prefiera no alojar el modelo, siete plataformas lo sirven como API: Baseten, Fireworks AI, Groq, Modal, Nebius, OpenRouter y Together AI.
Muse Spark Open corre sobre los motores de inferencia más usados del momento: SGLang, vLLM, TensorRT-LLM, Hugging Face Transformers, Apple MLX, llama.cpp y Unsloth, con optimizaciones específicas para silicio de AMD, Apple, Intel y Nvidia. Quien quiera afinar el modelo para un caso propio puede hacerlo con PyTorch, usando el framework de entrenamiento distribuido TorchTitan.
El framework MLX, usado para correr el modelo en Apple Silicon, es de código abierto desde 2023.
Contexto e historia
Meta Superintelligence Labs presentó la versión cerrada de Muse Spark hace unas semanas como el primer modelo de ese laboratorio. La novedad de octubre no es el modelo en sí, sino que Meta decidió abrir sus pesos y lo presenta, según el propio anuncio, como el modelo de pesos abiertos más capaz hecho en Estados Unidos hasta la fecha.
La jugada ubica al modelo de pesos abiertos de Meta en una carrera donde China venía marcando el ritmo: modelos abiertos como los de Moonshot AI, Z.ai o DeepSeek llevaban meses liderando los rankings de pesos abiertos en benchmarks como Artificial Analysis. Meta responde con un modelo grande, pero con un argumento distinto: que corre en hardware de escritorio, algo que ningún rival de ese tamaño ofrecía hasta ahora.
Detalles técnicos: cómo reparten la memoria los dos Mac Studio
El motivo técnico por el que dos Mac Studio alcanzan para correr un modelo de 816.000 millones de parámetros está en la arquitectura de memoria unificada de Apple. Cada chip M5 Ultra permite que la GPU integrada use hasta 512 GB de esa memoria, así que entre los dos equipos suman un terabyte disponible para guardar los pesos del modelo completo.
El ancho de banda es la otra mitad de la ecuación. El M5 Ultra entrega 1,2 TB/s de ancho de banda de memoria, una cifra que alcanza porque un modelo MoE no necesita leer los 816.000 millones de parámetros en cada paso: con el router eligiendo qué expertos activar, cada token solo necesita leer la porción de 42.000 millones de parámetros que le toca. MLX se encarga de repartir esa porción entre las dos máquinas para que, de cara al modelo, se comporten como una sola con el doble de memoria.
El M5 Ultra reparte hasta 512 GB de memoria unificada por chip entre CPU y GPU.
flowchart TD
A["Agente Muse Code"] --> B["Router MoE del modelo"]
B --> C["Mac Studio 1: M5 Ultra, 512 GB"]
B --> D["Mac Studio 2: M5 Ultra, 512 GB"]
subgraph "Memoria unificada combinada: ~1 TB"
C
D
end
Sobre esa base, Meta corrió pruebas agénticas completas en el hardware local, sin tocar la nube: usando Muse Code, su asistente de programación, sobre el modelo de pesos abiertos, el equipo aplicó cambios reales a Ice Cubes, un cliente de Mastodon de código abierto para iOS, con correcciones de bugs, funciones nuevas y compilación completa. En otra prueba, el modelo manejó DaVinci Resolve de punta a punta, revisando metraje fotograma por fotograma para encontrar los mejores momentos y recortarlos en formato vertical.
En el otro extremo del hardware, para quien corre el modelo en GPU de centro de datos, Meta publicó kernels GPU propios llamados Helion dentro de SGLang. Esos kernels autoajustables entregan entre 1,03 y 3,95 veces más velocidad a nivel de kernel en GPU de Nvidia y AMD, y un 3% de mejora de punta a punta en una AMD MI350.
OpciónCuándo usarlaVentajaLimitaciónAPI en la nube (Together AI, Fireworks, Groq, etc.)Para integrar el modelo sin gestionar hardwareSin inversión inicial, escala solaDepende de la disponibilidad y precio del proveedorPesos propios en GPU de centro de datosPara volumen alto o afinado con TorchTitanControl total y los kernels Helion aceleran la inferenciaRequiere GPU Nvidia o AMD de gama altaDos Mac Studio M5 Ultra en localPara correr el modelo completo sin nube ni GPU de servidorPrivacidad total, sin costo recurrente de APIEl hardware ronda varios miles de dólares y hay que mantenerlo
> 💡 Tip: antes de comprar el hardware, probá el modelo de pesos abiertos de Meta vía API (Together AI u OpenRouter, por ejemplo) para confirmar que el caso de uso lo justifica.
Impacto y análisis
Para un equipo de desarrollo, la diferencia práctica no es el tamaño del modelo sino dónde corre. Un modelo de 816.000 millones de parámetros servido por API no es noticia en 2026: lo distinto es que ahora existe la opción de tenerlo corriendo en una oficina, sin que el código fuente o el footage de un proyecto salgan de la red local. Eso importa para equipos con compromisos de confidencialidad o que simplemente no pueden mandar datos sensibles a un proveedor externo.
La licencia también es parte del cálculo. Meta libera el modelo bajo la Muse Model License, con términos comerciales limitados que permiten a buena parte de los desarrolladores usarlo gratis, aunque no es una licencia open source en el sentido estricto: empresas por encima de cierto umbral de usuarios suelen quedar fuera de la gratuidad, el mismo patrón que Meta ya usó con la familia Llama.
> 📌 Nota: pesos abiertos no es lo mismo que código abierto. La licencia de Meta restringe el uso comercial a partir de cierta escala, algo que conviene revisar antes de integrar el modelo de pesos abiertos de Meta en un producto.
El otro ángulo es la seguridad. Meta dice haber evaluado el modelo bajo su Meta Superintelligence Scaling Framework y haberle aplicado entrenamiento de seguridad adicional por tratarse de pesos abiertos, ya que una vez publicados los pesos no hay forma de revocarlos ni de parchear el modelo remotamente.
Qué sigue
Meta prometió un reporte de evaluación completo próximamente, además de la ficha de modelo ya publicada en Hugging Face. La compañía invita a compartir trabajos construidos con el modelo en los foros de Hugging Face, el canal que suele usar para medir adopción real antes de iterar en la siguiente versión.
Si la adopción crece, el próximo capítulo lógico es el mismo que ya atravesaron Llama y los modelos abiertos chinos: afinados de la comunidad para tareas específicas, y comparaciones de rendimiento frente a Kimi K3, GLM-5.2 o Qwen3.8 en benchmarks independientes.
📖 Resumen en Telegram: Ver resumen
Si tenés dos Mac Studio M5 Ultra a mano, el camino más corto para probarlo es instalar el adaptador MLX y descargar los pesos directo desde la colección de Muse Spark Open en Hugging Face.
Preguntas frecuentes
¿Qué es Muse Spark Open y quién lo publicó?
Es el modelo de pesos abiertos que Meta Superintelligence Labs derivó de su sistema Muse Spark, con 816.000 millones de parámetros totales y 42.000 millones activos por token.
¿Qué hardware necesito para correr el modelo de pesos abiertos de Meta en casa?
Según el anuncio de Meta, la configuración validada son dos Mac Studio con chip M5 Ultra, usando el framework MLX para repartir el modelo entre ambos equipos.
¿En qué se diferencia el modelo abierto de Meta de la versión cerrada de Muse Spark?
Comparten arquitectura y tamaño, pero la versión abierta publica los pesos para descarga y autoalojamiento, mientras que la versión cerrada solo se usa a través de los productos de Meta.
¿Qué licencia tiene el MoE abierto de Meta?
Se distribuye bajo la Muse Model License, con términos comerciales limitados que permiten uso gratuito a la mayoría de los desarrolladores.
¿Dónde se puede probar la IA abierta de Meta Superintelligence Labs?
Vía API en Baseten, Fireworks AI, Groq, Modal, Nebius, OpenRouter y Together AI, o descargando los pesos desde Hugging Face, Amazon SageMaker AI, Google Cloud Model Garden o Microsoft Foundry Models.
Referencias
- Meta AI Research: anuncio oficial del modelo, con benchmarks y detalles de licencia.- Hugging Face: colección con los pesos descargables del modelo.- GitHub: ml-explore/mlx: repositorio oficial del framework MLX usado para correr el modelo en Apple Silicon.- PyTorch: framework usado para personalizar el modelo con TorchTitan.
📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.
1 reaction
I missed this addition of 1-bit affine quantization to MLX 👀
PR: mlx#3161 t.co
10 likes · 2 reposts · 1,143 views
Grateful as @zcbenz merged my first contribution to #MLX a small M1 quantized-matmul dispatch tuning.
A year ago I decided to learn @Apple Silicon from scratch.
Getting reviewed and merged by the person who maintains MLX is humbling in the best way 🙌
mlx#4629
1 like · 34 views
How to install and tune oMLX on M5 Max 128 GB
---
title: How to install and tune oMLX on M5 Max 128 GB
published: true
description: Recipe for oMLX 0.6.4 on a Mac M5 Max with 128 GB: install, Claude Code and advanced settings with screenshots (Lightning MTP, VLM MTP and DFlash).
tags: macos, llm, ai, apple
canonical_url: jacar.es
cover_image: media.jacar.es
---
oMLX is an LLM inference server built on MLX, the framework Apple shipped in December 2023 for Apple Silicon. It adds continuous batching, two-tier KV cache (RAM + SSD), and an OpenAI- and Anthropic-compatible API.
On a Mac M5 Max with 128 GB of unified memory you can hold three or four large models at once with TurboQuant 3.5-bit on KV cache. That is enough to feed chat, agent and IDE in parallel. This guide collects the configuration tested in May 2026 to get the most out of that combination.
Quick answers
Which oMLX version do I use and how do I install it? Version 0.6.4 (released 29 August 2026, Apache 2.0). The direct route: download the .dmg from GitHub Releases, open it and drag to Applications. Or install via brew tap jundot/omlx https://github.com/jundot/omlx and brew install jundot/omlx/omlx.
The first load of the panel at http://localhost:8000/admin prompts for the API key. The full Homebrew route, including the service that starts on its own, is in installing, updating and uninstalling oMLX with Homebrew.
What TurboQuant setting makes sense on 128 GB? 3.5-bit. vLLM’s independent analysis published 11 May 2026 shows 3.5-bit matches full-precision quality with about 4x less KV-cache memory. On M5 Max that turns 128k context from "blows past available RAM" into "fits alongside other loaded models."
How much faster is it with the advanced 0.6.4 settings? Between 1.19x and 1.88x when generating code, and between 1.07x and 1.47x with 15,000 tokens of context, measured through the API on 15 September 2026. The biggest gain came from Lightning MTP on Qwen3.8-27B oQ4e, from 21.1 to 39.7 tok/s, and it needs a build of the model that keeps the MTP head. The details are in the advanced settings.
Which models can I load concurrently on 128 GB?
Primary: unsloth/Qwen3.6-35B-A3B-MLX-8bit (37.7 GB, MoE with 3B active). Fast helper: Qwen3-14B-Instruct-mlx-4bit (8 GB). Vision: Qwen2.5-VL-32B-mlx-4bit (18 GB).
Embeddings: BGE-M3-mlx (1.2 GB). Reranker: ModernBERT-base-mlx (150 MB). Comfortable sum: ~65 GB with headroom.
How do I point Claude Code at the local endpoint? Since 0.5.0 the short way is omlx launch claude, which exports the variables for you. By hand: export ANTHROPIC_BASE_URL=http://127.0.0.1:8000, ANTHROPIC_AUTH_TOKEN=<your_api_key> and the three ANTHROPIC_DEFAULT_*_MODEL variables (Opus/Sonnet/Haiku).
Launch with claude --bare to drop the system prompt to ~1,795 tokens. The dashboard’s Claude Code with oMLX section assembles the full command for you.
Does this replace Claude Opus 4.7? Not one for one. Claude Code is tuned for Claude’s tool-use format; a non-Claude model behind the endpoint loses reliability in agentic loops. Use this for offline work, sensitive data that should not leave the Mac, or as a fallback when api.anthropic.com is rate-limiting you.
Install
The easiest path: download the .dmg from GitHub Releases, open it and drag the app to Applications. The server starts in the background with a menu-bar icon.
Or install via Homebrew:
brew tap jundot/omlx https://github.com/jundot/omlx
brew install jundot/omlx/omlx
omlx start
omlx serve --model-dir ~/models
The first hit to http://localhost:8000/admin prompts for the API key. By default oMLX ships with no API key set (empty field), which is fine for local testing. If the instance only listens on 127.0.0.1 this is safe; the moment you expose it to the LAN or plan to share the endpoint, go to Settings → Auth & Info and set a long random string. The section accepts multiple keys at once.
!Login screen of the oMLX admin panel at localhost:8000/admin, with the API Key field empty and a "Stay signed in for 30 days" checkbox
Server settings for 128 GB
Settings → Global Settings holds the full server configuration. The decisions that matter on a 128 GB machine:
- Server → Host: Localhost only (127.0.0.1). If you open it to the LAN, put real auth in front first.
- Server → Port: 8000 by default.
- Resource Management → Memory Limit (Total): Auto. oMLX subtracts what macOS reserves; on 128 GB you end up around 110-114 GB for inference.
- Resource Management → Memory Limit (Models Only): Auto. Keeps a percentage for activations, KV cache and auxiliary processes.
- Resource Management → Hot Cache Limit: 10%. Intermediate RAM-tier KV cache. With TurboQuant enabled on the models that support it (see below), 10% is the value people running this setup in practice land on for 128 GB. With TurboQuant off and a single model loaded you can drop to Off, but you stop gaining.
- Resource Management → Cold Cache Limit (SSD Cache): 10%. Around 80 GB of SSD for cold tokens without saturating it.
- Resource Management → Max Concurrent Requests: 16. Comfortable for a single user with an agent, a chat session and an IDE pinging at once. Raise to 32 if you share with a small team.
- Resource Management → Idle Timeout: None. Keeps models warm; the first token arrives in well under a second instead of seconds later.
- Generation Defaults → Max Context Window: 256000 as the global default. The Qwen3.6 family holds up at long context, and with TurboQuant the effective RAM stretches enough to actually use it. Each model can be capped lower in Model Settings.
- Generation Defaults → Max Tokens: 64000. Upper bound per response. Going higher only matters if you plan to generate full books in one shot.
- Generation Defaults → Temperature: 1.0 for general use, 0.2-0.5 for code models.
- Model Settings → Experimental Features → TurboQuant KV Cache: enable at 3.5-bit on the large dense models. vLLM’s independent study published 11 May 2026 of Google’s TurboQuant shows 3.5-bit matches full-precision quality with roughly 4x less KV-cache memory. On M5 Max that makes 128k context fit on models where FP16 does not. On 0.6.4 it cannot be combined with VLM MTP, and in these tests it did not speed up generation; if speed is what you are after, see the advanced settings.
!oMLX Global Settings page: Host set to Localhost only (127.0.0.1), Port 8000, Memory Limit Total Auto (120 GB), Memory Limit Models Only Auto (108 GB), Hot Cache at 9% (12 GB), Cold Cache SSD at 10% (372 GB), Max Concurrent Requests 16, Idle Timeout None, Max Context Window 256000, Max Tokens 64000, Temperature 1.0, Top P 0.95
Model stack for multi-LLM
With 128 GB you have room to load one large primary model, a fast helper, a VLM, embeddings and a reranker concurrently. Download them from Models → Downloader by pasting the Hugging Face repo URL.
The recommendation holding up in May 2026 comes from people actually running Claude Code on oMLX on M5 Max (see Diego R. Baquero’s gist for the source): Unsloth’s Qwen 3.6 35B-A3B family in MoE with ~3B active parameters per token. On 128 GB the 8-bit fits without breaking a sweat:
- Primary (chat + code + reasoning): unsloth/Qwen3.6-35B-A3B-MLX-8bit (~37.7 GB). MoE 35B with 3B active. The all-rounder: long chat, code, agents. On 128 GB it fits alongside everything else without squeezing.
- More compressed alternative: unsloth/Qwen3.6-35B-A3B-UD-MLX-4bit (~21.6 GB) if you want two primary models loaded at once. Loses a notch of quality versus 8-bit but leaves room to experiment.
- Dense reasoning (when MoE falls short): Mistral-Large-2-123B-Instruct-mlx-4bit (~70 GB) or Llama-3.3-70B-Instruct-mlx-4bit (~40 GB). For deep reasoning in a single pass, where a dense architecture beats a small-activation MoE.
- Fast helper: Qwen3-14B-Instruct-mlx-4bit (~8 GB). For cheap tasks in agents, parsing and summaries.
- Vision: Qwen2.5-VL-32B-Instruct-mlx-4bit (~18 GB) covers OCR, image description and multimodal reasoning.
- Embeddings: BGE-M3-mlx (~1.2 GB), dense + sparse + multi-vector in a single model.
- Reranker: ModernBERT-base-mlx (~150 MB) to close the loop on a decent RAG pipeline.
Comfortable concurrent load with Qwen3.6 35B-A3B 8-bit as primary + 14B helper + VL-32B + embeddings + reranker: around 65-70 GB. Add Mistral Large 2 123B on top for dense reasoning and you hit ~135 GB nominal. The LRU policy moves idle models to SSD, so the cohabitation works in practice for sessions that do not pin everything at once. Pin the primary model from Model Manager so it never gets evicted.
Point Claude Code at the local endpoint
oMLX exposes an Anthropic-compatible API at http://127.0.0.1:8000. In current versions the routes live under /v1: POST /v1/messages and POST /v1/messages/count_tokens. Claude Code respects ANTHROPIC_BASE_URL, so pointing the CLI at your Mac is a matter of exporting environment variables. First, turn off the attribution header in Claude Code’s global config so the gateway does not bolt extra noise onto the prompt:
Since 0.5.0 there is a shortcut that does all of this for you and did not exist when this recipe was written:
omlx launch claude
It sets ANTHROPIC_BASE_URL against your server, puts the configured key in ANTHROPIC_AUTH_TOKEN (or omlx if there is none), empties ANTHROPIC_API_KEY to force the base URL to be used, raises API_TIMEOUT_MS to 3,000,000 and disables non-essential traffic. It also sets CLAUDE_CODE_MAX_CONTEXT_TOKENS and CLAUDE_CODE_AUTO_COMPACT_WINDOW to what the engine can actually do, which is what the context-scaling option used to do by hand. It enforces a minimum window of 48,000 tokens and will not start if the chosen model falls short. There are equivalent commands for Codex, OpenCode, OpenClaw, Hermes and Pi, described in the dashboard and command line guide.
The manual recipe below still works and is the one to use if you want fine control over each variable.
~/.claude/settings.json:
{
"env": {
"CLAUDE_CODE_ATTRIBUTION_HEADER": "0"
}
}
Then the startup command:
export ANTHROPIC_BASE_URL=http://127.0.0.1:8000
export ANTHROPIC_AUTH_TOKEN=<your_api_key>
export ANTHROPIC_DEFAULT_OPUS_MODEL=unsloth/Qwen3.6-35B-A3B-MLX-8bit
export ANTHROPIC_DEFAULT_SONNET_MODEL=unsloth/Qwen3.6-35B-A3B-MLX-8bit
export ANTHROPIC_DEFAULT_HAIKU_MODEL=Qwen3-14B-Instruct-mlx-4bit
export ANTHROPIC_DEFAULT_MODEL=unsloth/Qwen3.6-35B-A3B-MLX-8bit
export API_TIMEOUT_MS=600000
export CLAUDE_CODE_USE_BEDROCK=0
export DISABLE_NONESSENTIAL_TRAFFIC=1
claude --bare
The --bare flag skips hooks, LSP, plugin sync and auto-memory, dropping Claude Code’s system prompt to around 1,795 tokens (versus the thousands it uses with everything loaded). For offline work against a local model that is the sensible default: every token in the system prompt is bandwidth your Mac would otherwise burn. Drop the flag when you point back at api.anthropic.com.
The oMLX dashboard builds this command for you in the Claude Code with oMLX section: pick Opus, Sonnet and Haiku from three dropdowns and copy the ready-to-paste command. Set Context scaling for Claude Code to 64000: Claude Code requests 200k tokens by default, but a local model behaves better with a 64k ask than a 200k ask it cannot honor. The option scales the reported counts so auto-compact triggers at the target size.
!oMLX dashboard with the Claude Code with oMLX section fully configured: Local mode active, Opus mapped to Huihui-Qwen3.6-35B-A3B-Claude-4.7-Opus-abliterated-mlx-8bit, Sonnet to Qwen3.6-40B-Claude-4.6-Opus-Deckard-Heretic-Uncensored-Thinking-8bit, Haiku to Qwen3.5-9B-Claude-4.6-HighIQ-INSTRUCT-HERETIC-UNCENSORED-MLX-mxfp8, Context scaling for Claude Code enabled with target 200000, plus pre-built commands for Codex, OpenCode and other integrations below
A real caveat: Claude Code is tuned to Claude’s tool-use format and response patterns. A non-Claude model behind ANTHROPIC_BASE_URL works for autocomplete and reasoning, but you will see drops in tool-call reliability and in agentic loops. Use this for offline work, sensitive data that should not leave the Mac, or as a fallback when api.anthropic.com is rate-limiting you. It is not a 1:1 substitute for Claude Opus 4.7.
SSH from another Mac
If oMLX runs on a Mac Studio and you want to use Claude Code from your MacBook, SSH port forwarding is the bridge:
ssh -L 8000:localhost:8000 user@mac-studio
Once connected, 127.0.0.1:8000 on your laptop points to the Studio’s oMLX. Save this script as claude-local.sh:
#!/bin/bash
export ANTHROPIC_BASE_URL='http://localhost:8000'
export ANTHROPIC_AUTH_TOKEN=''
export ANTHROPIC_DEFAULT_OPUS_MODEL='Qwen3.6-35B-A3B-MLX-8bit'
export ANTHROPIC_DEFAULT_SONNET_MODEL='Qwen3.6-35B-A3B-MLX-8bit'
export ANTHROPIC_DEFAULT_HAIKU_MODEL='Qwen3-14B-MLX-4bit'
export ANTHROPIC_DEFAULT_MODEL='Qwen3.6-35B-A3B-MLX-8bit'
export API_TIMEOUT_MS=600000
export CLAUDE_CODE_USE_BEDROCK=0
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
claude --bare --dangerously-skip-permissions
Make it executable with chmod +x claude-local.sh and run it from the SSH session. The default API key is the empty field. If you set one in Settings → Auth & Info, replace '' with it.
--bare drops Claude Code’s system prompt to ~1,795 tokens. --dangerously-skip-permissions bypasses interactive permission prompts that don’t make sense in an automated pipeline.
Benchmark on real hardware
Bench → Performance runs tests on your actual hardware. The panel covers prefill at eight prompt sizes (pp1024, pp4096, pp8192, pp16384, pp32768, pp65536, pp131072, pp200000) and continuous batching at 2x, 4x and 8x concurrency. Results go to the public oMLX leaderboard, and My Submissions shows yours next to other machines on the same profile. On 0.6.4 the upload happens automatically when each run finishes; how to avoid it is in the 0.6.4 benchmarks.
Reference numbers measured on M5 Max 40-core with 128 GB on oMLX 0.3.8, in May 2026. Not extrapolated (the omlx.ai leaderboard already has M5 Max submissions, and vLLM’s independent TurboQuant study published 11 May 2026 confirms the figures for the large dense models):
- Qwen 3.6 35B-A3B 8-bit (MoE, 3B active): 65-80 tok/s on decode at short context. The model that most changes day-to-day use on this machine. Live benchmarks from this setup:
| Test | TTFT | Decode TPS | E2E | Peak Mem |
| --- | --- | --- | --- | --- |
| pp1024/tg128 | 577 ms | 72.4 tok/s | 2.35s | 38.5 GB |
| pp4096/tg128 | 1,293 ms | 83.1 tok/s | 2.83s | 36.2 GB |
| pp8192/tg128 | 2,995 ms | 82.1 tok/s | 4.55s | 36.5 GB |
| pp32768/tg128 | 13,785 ms | 15.2 tok/s | 22.18s | 40.9 GB |
Continuous batching (pp1024 / tg128):
| Batch | Decode TPS | Speedup |
| --- | --- | --- |
| 1x (baseline) | 72.4 tok/s | 1.00x |
| 2x | 87.1 tok/s | 1.20x |
| 4x | 123.8 tok/s | 1.71x |
| 8x | 237.7 tok/s | 3.28x |
- Llama 3.3 70B 4-bit: 14-18 tok/s on decode.
- Mistral Large 2 123B 4-bit with TurboQuant 3.5-bit on KV cache: 8-11 tok/s on decode. The number that matters at 128k context is peak memory, ~74 GB (FP16 KV blows past 128 GB). dasroot.net’s long-context M5 Max experiment documents the same peaks for the 104B dense model.
- gpt-oss-20b MXFP4-Q4 on M5 Max 40c per the public oMLX benchmark submission lands around 100 tok/s on decode.
The tables above are our own measurement on 0.3.8, and on 15 September 2026 I re-ran the tests on 0.6.4 on this same machine. With no acceleration at all, this model goes from 15.2 to 77.9 tok/s with a 32k prompt. Speculative decoding adds between 1.07x and 1.88x, depending on the model and the context. The new tables are in the 0.6.4 benchmarks.
vLLM measured the TurboQuant KV-cache compression at 4.41x in May 2026. That figure is what separates "I have 128k context but it does not fit" from "I have it and can load two other models alongside it". At 4-bit and 3.5-bit, quality stays close to full precision; at 3-bit you start to feel it in code and long-form reasoning.
!oMLX Bench → Performance Benchmark page with Qwen3.6-35B-A3B-MLX-8bit selected, Single Request Test checkboxes (pp1024, pp4096, pp8192, pp32768) and Continuous Batching Test checkboxes (2x, 4x, 8x), Run Benchmark button, and My Submissions link
Advanced settings for maximum performance on 0.6.4
On 0.6.4, the biggest speed lever on this M5 Max is speculative decoding, and the right method depends on the model. Measured through the API at temperature 0.6, Qwen3.8-27B oQ4e went from 21.1 to 39.7 tok/s with Lightning MTP. Qwen3.6-35B-A3B 8-bit went from 79.7 to 117.5 tok/s with VLM MTP. Everything in this section was measured on 15 September 2026 on the machine this guide is about, and the method and the full tables are in the 0.6.4 benchmarks section.
Which acceleration method to use with each model
oMLX 0.6.4 has three speculative decoding methods and allows only one per model:
- Lightning MTP: uses the multi-token prediction (MTP) head that ships inside the weights. It only works if the conversion kept the mtp.* tensors.
- VLM MTP: uses an external MTP drafter under 2 GB, trained for that base model. It only kicks in when no other request is running.
- DFlash: uses a z-lab block-diffusion drafter that proposes up to 16 tokens per pass. It serves requests one at a time and keeps its own prefix cache.
In all three the large model verifies every proposed token, so the output comes from the large model, not from the drafter. This is what each combination measured on this M5 Max:
| Model | Method | What to download | Size | Decode writing code | Decode with 15,000 tokens of context |
| --- | --- | --- | --- | --- | --- |
| Qwen3.6-35B-A3B 8-bit | VLM MTP | mlx-community/Qwen3.6-35B-A3B-MTP-bf16 | 1.7 GB | 79.7 → 117.5 tok/s | 84.0 → 90.7 tok/s |
| Qwen3.6-35B-A3B oQ4e | Lightning MTP | Jundot/Qwen3.6-35B-A3B-oQ4e-mtp (full model) | 21.6 GB | 100.2 → 119.2 tok/s | 100.2 → 115.5 tok/s |
| Qwen3.8-27B oQ4e | Lightning MTP | Jundot/Qwen3.8-27B-oQ4e-mtp (full model) | 17.0 GB | 21.1 → 39.7 tok/s | 21.4 → 27.2 tok/s |
| Qwen3.8-27B 4-bit | VLM MTP | mlx-community/Qwen3.8-27B-MTP-bf16 | 0.87 GB | 24.1 → 34.5 tok/s | 21.7 → 23.2 tok/s |
| Gemma 4 12B 8-bit | VLM MTP | mlx-community/gemma-4-12B-it-assistant-bf16 | 0.88 GB | 27.4 → 50.5 tok/s | 26.6 → 39.2 tok/s |
| gpt-oss-20b | None | – | – | – | – |
For Qwen3.6-35B-A3B, the oQ4e build with Lightning MTP was the fastest in both tests and takes about 15 GB less than the 8-bit one. With long context and prose answers every gain shrinks, because the large model accepts fewer draft tokens, but no configuration was slower than its baseline.
If a Qwen model’s settings show the warning Config declares MTP layers but the weight files contain neither mtp.* tensors nor native nextn layers, the conversion stripped the MTP head. The default mlx-lm converters do that: it applied to the Qwen3.6-35B-A3B builds from mlx-community and the Qwen3.8-27B builds from lmstudio-community installed on this Mac. You have two ways out: download a build that keeps the head, such as the oQ4e-mtp builds published by the oMLX author, or turn on VLM MTP with an external drafter.
Downloading the drafters
Drafters download like any other model. In Models → Downloader, paste the Hugging Face repository id and start the download. oMLX recognises drafters by their type and marks them as helpers; with Hide Helper Models on, they also stay out of the API’s model list.
!oMLX 0.6.4 Download from HuggingFace form with the repository id mlx-community/Qwen3.6-35B-A3B-MTP-bf16 filled in, the HF Token field empty and the Download button
Turning on Lightning MTP
With a model that keeps its MTP head, open Settings → Model Settings, click the model’s gear icon and turn on Lightning MTP in the Acceleration block. When you save, oMLX reloads the model.
On Qwen the default is 3 draft tokens per cycle, and an adaptive controller adjusts it from each response’s acceptance rate. The oMLX log writes one line per response with that rate: on Qwen3.6-35B-A3B oQ4e it sat around 72-77%, with 2.5 to 2.9 tokens emitted per cycle. With greedy sampling the output matches the model without MTP, apart from tiny numerical differences. With temperature, rejection sampling keeps the large model’s distribution.
!Acceleration block of the Jundot/Qwen3.8-27B-oQ4e-mtp model settings in oMLX 0.6.4, with the Lightning MTP switch turned on
Turning on VLM MTP with an external drafter
In the same Acceleration block, turn on VLM MTP, pick the drafter from the drop-down and save. Three conditions in the 0.6.4 code are worth knowing first:
- The model has to load on the vision engine. The multimodal Qwen3.6 and Qwen3.8 builds do so by default, but on a text-only model the setting is silently ignored.
- It cannot be combined with TurboQuant or with repetition or presence penalties. The built-in Qwen presets set presence_penalty to 1.5, which turns it off.
- It only speeds up a request when nothing else is running. With concurrent requests, oMLX falls back to normal batching.
!VLM MTP card in the mlx-community/Qwen3.6-35B-A3B-8bit settings in oMLX 0.6.4, turned on with the Qwen3.6-35B-A3B-MTP-bf16 drafter and the block size left empty, which means 4 tokens
You can apply the same change through the admin API, which is handier when you configure more than one model or script it. The first command logs in with your key and stores the cookie; the second updates the model’s settings:
curl -c omlx.cookies -X POST http://127.0.0.1:8000/admin/api/login \
-H "Content-Type: application/json" \
-d '{"api_key": "your_api_key_here"}'
curl -b omlx.cookies -X PUT \
http://127.0.0.1:8000/admin/api/models/Qwen3.6-35B-A3B-8bit/settings \
-H "Content-Type: application/json" \
-d '{"vlm_mtp_enabled": true,
"vlm_mtp_draft_model": "Qwen3.6-35B-A3B-MTP-bf16"}'
The model name in the path and the drafter name are the ids shown in the model list. For Lightning MTP the body is {"mtp_enabled": true}. The endpoint rejects any field it does not know, so a typo does not slip through.
DFlash only pays off with short prompts
DFlash was the fastest method with 1,000-token prompts and the worst with long context. In the built-in benchmark, Qwen3.6-35B-A3B 8-bit rose from 92 to 112 tok/s with a 1k prompt, but dropped from 78 to 17 tok/s with 32k. Qwen3.8-27B 4-bit dropped from 23.5 to 6.3 tok/s with 32k. If you use Claude Code, which sends tens of thousands of tokens of context every turn, leave it off.
For short-prompt work such as chat or autocomplete, download the matching z-lab drafter: z-lab/Qwen3.6-35B-A3B-DFlash (0.77 GB), z-lab/Qwen3.8-27B-DFlash2 (3.85 GB) or z-lab/gemma4-12B-it-DFlash (1.46 GB). Turn it on in Acceleration → DFlash and quantise the drafter to 4 bits. On the 27B, the 4-bit drafter gave 48.2 tok/s with a 1k prompt against 36.9 tok/s in bf16, and used 2.5 GiB less.
It has two more limits. It serves one request at a time. And although dflash_max_ctx sends long requests to the normal engine, after the first one DFlash stays off until you reload the model.
!oMLX 0.6.4 DFlash card with the Qwen3.6-35B-A3B-DFlash drafter selected, drafter quantisation on with 4-bit weights, 16-bit activations and group size 64, and the max context threshold left unlimited
Prefill on the Neural Engine: no gain with Lightning MTP
Qwen ANE Prompt Processing splits part of the Qwen3.5, 3.6 and 3.8 prefill between Apple’s Neural Engine (ANE) and the GPU. The Tune for this Mac button measures the best split on your machine and saves nothing until you apply the result. On Qwen3.8-27B oQ4e it recommended sending 35% of the MLP channels and 37.5% of the GDN layers to the ANE. With that split, prefill on an 8,193-token prompt went from 452 to 510 tok/s, 12.8% more.
That 12.8% did not hold with the full model. With the split applied and Lightning MTP on, the built-in benchmark measured 505 tok/s of prefill with a 4k prompt against 499 without the ANE, and 422 against 471 with 32k. Through the API, the first token with 15,100 tokens of context took 34.8 s, against 35.2 s without the ANE. In exchange the loaded model grew from 16.0 to 20.7 GB and loading went from 2 to 20 s, so on this Mac I left it off.
Before tuning it on an M5 Max, turn off Use both ANEs in the model settings. That mode is meant for the M3 Ultra, which joins two chips and has two ANEs. It only speeds up prompts of 2,048 tokens or more; generation stays on the GPU.
The project itself documents three costs. It is not bit-exact, because the ANE runs weights requantised to INT8. It relies on private Apple interfaces that a macOS update can break, and it lengthens model load. If you turn it on anyway, also turn on Reuse Compiled ANE Programs under Global Settings → Advanced so it does not recompile on every start.
!oMLX 0.6.4 Qwen ANE Prompt Processing card with the split the tuner proposed: 2048-token block, padding from 1816, 0.35 of the MLPs and 0.375 of the GDN layers on the ANE, Use both ANEs off and the Tune for this Mac button
The global settings that matter
oMLX’s default global configuration already suits a single user, and only four settings change anything measurable:
- Kernel memory limit: macOS caps the memory the GPU can wire by default, and the dashboard shows a red warning when that cap is below what oMLX could use. Run sudo sysctl iogpu.wired_limit_mb=124518, which on 128 GB is RAM minus 5%, and restart oMLX, which applies it at start-up. The value is lost when the Mac restarts.
- Prefill Priority: set it to Speed. When memory is tight, Max Context shrinks prefill down to 32 tokens per step so the prompt fits, while Speed keeps full speed and rejects what does not fit.
- Hide Helper Models: turn it on so drafters stay out of /v1/models and no client picks one by mistake.
- Max Concurrent Requests: keep it between 8 and 16, and do not lower it to 1. The scheduler admits no new request while as many cache writes are pending as that limit.
Burst Decode can stay on Balanced. Aggressive holds tokens for up to 200 ms before sending them, against 100 ms, so the client sees the first token later. One last warning: clicking Save Settings unloads every loaded model, even if you did not touch the cache, because the dashboard resends every field.
!Resource Management section of the oMLX 0.6.4 global settings: Balanced memory guard, hot cache at 9% (12 GB), SSD cache at 10% (372 GB), 16 concurrent requests, Chunked Prefill off, Prefill Priority set to Speed and no idle timeout
What does not make it faster
Three settings with promising names did not improve single-user speed:
- TurboQuant KV: with Lightning MTP on Qwen3.8-27B oQ4e, turning it on at 4 bits left peak memory unchanged with a 32k prompt (24.4 GiB) and lowered decode from 35.4 to 30.3 tok/s. In these hybrid models only the full-attention layers keep a KV cache, so there is little to compress. It saves memory on huge contexts; it does not make generation faster.
- SpecPrefill: brings the first token forward by having a small model drop the prompt tokens it scores as unimportant. The large model never sees them, so it loses information by design.
- Chunked Prefill: it only cuts time to first token when requests overlap.
Benchmarks on 0.6.4 on this M5 Max
With 0.6.4 and the settings above, this M5 Max generates between 1.19 and 1.88 times more tokens per second when writing code. With 15,000 tokens of context, the gain sits between 1.07 and 1.47 times. Without any acceleration, Qwen3.6-35B-A3B 8-bit now holds 77.9 tok/s with a 32k prompt, where 0.3.8 dropped to 15.2 tok/s. The tests date from 15 September 2026.
How it was measured
There are two test sets, because the built-in benchmark shows how speed changes with context but cannot compare with and without acceleration:
- Through the API: streaming requests to /v1/chat/completions at temperature 0.6 with reasoning off. There are two tests. In the short one, each configuration answers three coding tasks (Python, TypeScript and Go) twice, with 768-token answers. In the long one, it answers two questions about roughly 15,100 tokens of source code, with 512-token answers. Timing is taken on the client.
- Built-in benchmark: started from POST /admin/api/bench/start, three repeats per configuration and the median, prompts of 1,025, 4,097, 16,385 and 32,769 tokens, 256 generated tokens and the code_python corpus. It always samples greedily, which is the best case for speculative decoding.
Every request starts with a unique prefix, so none of them benefits from the prefix cache. Tables show the median, with the minimum and maximum in brackets when they spread more than 10% from the median in the API tests, or more than 25% in the built-in benchmark.
The tests ran back to back for about three hours, with other applications open. The built-in benchmark records macOS thermal pressure, and 194 of its 242 measurements were taken at the Heavy level, the third of five (Nominal, Moderate, Heavy, Trapping and Sleeping). Only the first gpt-oss batch ran cold. Its prefill with a 16k prompt gave 3,140 tok/s cold and 1,755 tok/s hot.
Read the absolute figures as those of a hot Mac. The comparisons with and without each setting were run one after the other, so the relative gains are more reliable than the absolute values.
Two details of the 0.6.4 built-in benchmark change how to read it. First, when it finishes it uploads the results to omlx.ai with the chip, the models and an identifier derived from the Mac, and the dashboard has no option to prevent it. With a local model, the only way to keep the results on the Mac is to tick ANE-aligned prompts (+1 token). That is the checkbox I used, which is why the prompts are one token longer.
Second, it loads vision models on the text-only engine unless MTP is on, so comparing with and without acceleration mixes two engines. With short prompts, Qwen3.6-35B-A3B oQ4e scored 49 tok/s on that engine and 100 tok/s through the API. That is why the gains in this guide come from the API tests.
Speed through the API with coding answers
| Model | Configuration | Decode | First token | 768-token answer |
| --- | --- | --- | --- | --- |
| Qwen3.6-35B-A3B 8-bit | No acceleration | 79.7 tok/s | 0.37 s | 10.0 s |
| | VLM MTP | 117.5 (110.5-125.5) tok/s | 0.37 s | 6.9 s |
| Qwen3.6-35B-A3B oQ4e-mtp | No acceleration | 100.2 tok/s | 0.34 s | 8.0 s |
| | Lightning MTP | 119.2 (108.5-139.1) tok/s | 0.46 s | 6.9 s |
| Qwen3.8-27B 4-bit | No acceleration | 24.1 tok/s | 0.79 s | 32.6 s |
| | VLM MTP | 34.5 tok/s | 0.65 s | 22.8 s |
| Qwen3.8-27B oQ4e-mtp | No acceleration | 21.1 tok/s | 0.82 s | 37.3 s |
| | Lightning MTP | 39.7 tok/s | 0.94 s | 20.4 s |
| Gemma 4 12B 8-bit | No acceleration | 27.4 tok/s | 0.84 s | 28.9 s |
| | VLM MTP | 50.5 (47.5-52.7) tok/s | 0.66 s | 15.9 s |
Speed through the API with 15,000 tokens of context
| Model | Configuration | Decode | First token |
| --- | --- | --- | --- |
| Qwen3.6-35B-A3B 8-bit | No acceleration | 84.0 tok/s | 6.20 s |
| | VLM MTP | 90.7 tok/s | 5.96 s |
| Qwen3.6-35B-A3B oQ4e-mtp | No acceleration | 100.2 tok/s | 5.83 s |
| | Lightning MTP | 115.5 tok/s | 6.14 s |
| Qwen3.8-27B 4-bit | No acceleration | 21.7 tok/s | 34.5 s |
| | VLM MTP | 23.2 tok/s | 32.6 s |
| Qwen3.8-27B oQ4e-mtp | No acceleration | 21.4 tok/s | 31.3 s |
| | Lightning MTP | 27.2 (24.4-30.1) tok/s | 35.2 s |
| Gemma 4 12B 8-bit | No acceleration | 26.6 tok/s | 20.1 s |
| | VLM MTP | 39.2 tok/s | 19.3 s |
Each row is the median of two 512-token answers over the same code context. With this context the large model accepted fewer draft tokens than when writing code: on Qwen3.8-27B 4-bit, the VLM MTP gain fell from 1.43x to 1.07x.
Built-in benchmark by prompt length
Generation speed in tok/s by prompt length, with prefill at 4k and MLX peak memory, which includes the weights:
| Model and configuration | 1k | 4k | 16k | 32k | Prefill 4k | Peak |
| --- | --- | --- | --- | --- | --- | --- |
| Qwen3.6-35B-A3B 8-bit, no acceleration | 92.1 | 91.1 | 84.1 | 77.9 | 2,426 | 38.7 GiB |
| Qwen3.6-35B-A3B 8-bit, VLM MTP | 118.3 | 106.6 | 100.2 | 94.4 | 3,113 | 41.8 GiB |
| Qwen3.6-35B-A3B 8-bit, DFlash (4-bit drafter) | 112.4 | 110.5 | 32.6 (23.3-35.0) | 16.6 | 2,734 | 38.2 GiB |
| Qwen3.6-35B-A3B oQ4e-mtp, no acceleration | 49.3 | 46.1 | 45.2 | 42.2 | 2,258 | 23.2 GiB |
| Qwen3.6-35B-A3B oQ4e-mtp, Lightning MTP | 77.2 (47.0-101.7) | 85.8 | 87.9 (85.7-116.6) | 82.8 (54.7-99.5) | 2,014 | 24.6 GiB |
| Qwen3.6-35B-A3B oQ4e-mtp, DFlash (4-bit drafter) | 142.7 | 86.3 | 39.2 | 17.3 | 3,373 | 22.7 GiB |
| Qwen3.8-27B 4-bit, no acceleration | 30.6 | 27.3 | 25.0 | 23.5 | 525 | 21.7 GiB |
| Qwen3.8-27B 4-bit, VLM MTP | 39.3 | 36.6 | 36.4 | 30.9 | 619 | 25.4 GiB |
| Qwen3.8-27B 4-bit, DFlash2 (4-bit drafter) | 48.2 (38.7-51.1) | 43.7 | 35.8 (22.0-39.0) | 6.3 | 561 | 22.8 GiB |
| Qwen3.8-27B 4-bit, DFlash2 (bf16 drafter) | 36.9 | 35.1 | 22.3 | 5.9 | 568 | 25.3 GiB |
| Qwen3.8-27B oQ4e-mtp, no acceleration | 27.0 | 26.4 | 24.4 | 21.4 | 545 | 22.2 GiB |
| Qwen3.8-27B oQ4e-mtp, Lightning MTP | 48.0 (42.6-58.6) | 37.7 | 41.1 | 35.4 | 499 | 24.4 GiB |
| Qwen3.8-27B oQ4e-mtp, Lightning MTP + TurboQuant 4-bit | 42.2 | 37.7 | 39.7 | 30.3 | 547 | 24.4 GiB |
| Qwen3.8-27B oQ4e-mtp, Lightning MTP + ANE prefill | 35.6 | 33.1 | 35.8 | 30.9 | 505 | 28.3 GiB |
| Qwen3.8-27B oQ4e-mtp, DFlash2 (4-bit drafter) | 41.4 | 33.8 | 18.8 (16.6-21.9) | 5.8 | 458 | 23.3 GiB |
| Gemma 4 12B 8-bit, no acceleration | 31.0 | 30.5 | 30.1 | 28.8 | 955 | 14.5 GiB |
| Gemma 4 12B 8-bit, VLM MTP | 65.6 | 42.7 | 50.6 | 49.0 | 940 | 15.5 GiB |
| Gemma 4 12B 8-bit, DFlash (4-bit drafter) | 92.4 (35.5-158.0) | 34.4 | 53.4 | 51.2 | 997 | 18.1 GiB |
| gpt-oss-20b, no acceleration | 120.2 | 106.5 | 100.4 | 78.8 | 3,491 | 12.4 GiB |
DFlash figures with a 1k prompt varied widely between repeats: on Gemma 4 12B they were 35.5, 92.4 and 158.0 tok/s. Speed depends on how many draft tokens the large model accepts, and that changes with the text it has to continue.
Compared with 0.3.8
The May table and this one are not fully comparable. That one was measured on the Qwen3.6-35B-A3B-MLX-8bit copy with 128 output tokens; this one, on the mlx-community copy with 256. Even so, the long-context improvement is too large to come from those differences:
| Qwen3.6-35B-A3B 8-bit, built-in benchmark | 0.3.8 (May) | 0.6.4 (September) |
| --- | --- | --- |
| Decode with a 1k prompt | 72.4 tok/s | 92.1 tok/s |
| Decode with a 4k prompt | 83.1 tok/s | 91.1 tok/s |
| Decode with a 32k prompt | 15.2 tok/s | 77.9 tok/s |
| First token with a 1k prompt | 577 ms | 298 ms |
End-to-end verification
A curl call to the OpenAI-compatible API to confirm the server is alive and a model responds:
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Authorization: Bearer your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen3-Coder-30B-A3B-Instruct-mlx-8bit",
"messages": [{"role": "user", "content": "Hello from oMLX"}]
}'
And from Claude Code, once you have exported the variables from the previous section:
claude --print "Are you running locally?"
The reply should come back from local without touching api.anthropic.com. To confirm at the network level, open Activity Monitor → Network, filter by the claude process and check that the outbound connection is against 127.0.0.1:8000.
!oMLX Chat view at localhost:8000/admin/chat with the model dropdown open, listing the four chat-completion models available locally (both Huihui-Qwen3.6-35B-A3B-Claude-4.7-Opus variants, Qwen3.5-9B-Claude-4.6-HighIQ, and Qwen3.6-40B-Claude-4.6-Opus-Deckard-Thinking); the embeddings and reranker models do not appear here because they are not chat-completion
What has changed since 0.3.8
This guide was written against oMLX 0.3.8 in May 2026. The current version is 0.6.4, released on 29 August 2026, and more than a dozen releases in between touch exactly what is tuned here. What follows is the summary of what changed, with the figures attributed to whoever measured them.
Lightning MTP speculative decoding
This is the change that affects this configuration most. Version 0.5.0, from 10 July 2026, added native depth-k speculative decoding for Qwen3.6-27B, Qwen3.6-35B-A3B, DeepSeek-V4-Flash and GLM-5.2.
On an Apple M3 Ultra the project measured Qwen3.6-35B-A3B going from 89.6 to 140.4 tok/s, a 1.57x gain. Qwen3.6-27B went from 35.0 to 55.1 tok/s. On this M5 Max, measured through the API on 15 September 2026, Lightning MTP took Qwen3.8-27B oQ4e from 21.1 to 39.7 tok/s. How to turn it on, and when another method is the better choice, is in the advanced settings.
Custom kernels, with a note for M5
The same 0.5.0 added custom compute kernels for DeepSeek V4, Qwen3.5/3.6 and GLM-5.2, with prefill improvements measured on M3 Ultra. They are 45% on DeepSeek-V4-Flash (313.3 to 455.8 tok/s at 64k context), 33% on Qwen3.6-27B and 99% on GLM-5.2 at 32k. What matters for this machine is that dispatch is NAX-aware precisely to avoid regressions on the M5 series, so it is worth confirming the effect rather than assuming it.
oQ and oQe quantisation
oMLX no longer depends only on the weights you download: it brings its own quantisation scheme. Version 0.5.0 added oQe, an activation-importance calibration pass with expert-coverage tracking for MoE models. In the project’s measurements oQ4e improves average accuracy over oQ4 while staying in the same disk-size class. That is 83.88% against 82.83% on Qwen3.6-35B-A3B, and 73.09% against 70.01% on Qwen3.5-9B.
The neural engine and cold starts
The 0.6.3 branch worked on the prefill path over Apple’s neural engine. Two of the project’s figures matter day to day. Bank compilation memory dropped from 35.8 GB to 4.7 GB, and an optional persistent compile cache cuts fresh-process startup by 53% to 66%. Version 0.6.4 continued down that road and improved Qwen3.8-Flash-Next prompt processing by 33.5%, with 24.2% less total request time at 32k, again on M3 Ultra.
New endpoints
The OpenAI- and Anthropic-compatible pair has been joined by /v1/rerank for document reranking, /v1/responses for Codex compatibility and /v1/mcp/tools for Model Context Protocol. With an embedding model and a reranker loaded at once, a whole retrieval pipeline fits on the same server. The full breakdown is in the guide to the API, the key and the port.
Sub-key authentication
Where there used to be a list of equivalent keys, there are now three credentials with different powers. A main key opens the API and the panel; sub-keys only call the API and cannot log into the panel or change settings; panel session tokens are held in a signed cookie. If you are handing access to more than one application, hand out sub-keys.
The setting names
The memory configuration in the earlier section still holds as judgement, but the keys are named differently in ~/.omlx/settings.json. The memory ceiling is now memory_guard_tier, with four levels (safe, balanced, aggressive and custom), and the bespoke value goes in memory_guard_custom_ceiling_gb. By default the ceiling is system RAM minus 8 GB. The two cache tiers are hot_cache_max_size and ssd_cache_max_size, and the concurrency limit is max_concurrent_requests.
Watch one of them: hot_cache_max_size ships as "0", which disables the hot cache in RAM. I go through it in benchmarks and the memory ceiling.
Other pieces that did not exist
Profiles save named bundles of settings over a single model, exposed as model:profile and at no extra memory cost. Aliases rename a model as far as the API is concerned. And there is experimental distributed inference, splitting a model’s layers across more than one Mac over SSH according to each machine’s memory, accounting for whether the link is Thunderbolt or 10GbE. The first two are in the dashboard and command line guide.
Sources and further reading
- Diego R. Baquero, "Running Claude Code with a local LLM": the tested configuration recipe this post’s settings are based on (TurboQuant 3.5-bit, 64k context, attribution off, Qwen 3.6 35B-A3B models).
- A First Comprehensive Study of TurboQuant, vLLM blog, 11 May 2026: independent analysis confirming 3.5-bit matches full-precision quality.
- omlx.ai: public leaderboard with real submissions from M5 Max and other machines.
- Maxing Out M5 Max Context Windows: Memory Fragmentation and TurboQuant Benchmarks (dasroot.net, April 2026): the other detailed experiment on long-context M5 Max with TurboQuant.
- Repositories: jundot/omlx, ml-explore/mlx, ml-explore/mlx-lm, huggingface.co, huggingface.co.
If you came here wondering what this actually is and how it differs from Apple’s MLX framework, I separate the two in what is oMLX. For a cross-platform alternative outside Apple Silicon, the Ollama local LLM guide covers the model catalogue and OpenAI-compatible API. To wire the local endpoint into an agent, the Anthropic SDK tutorial and the MCP multi-vendor patterns already talk to this same format with no code changes.
Related resources
- Calculator: which LLM fits your GPU?
- Learning path: Local LLMs
Frequently asked questions
Do I need to set an API key to use oMLX locally only?
No. oMLX ships with the API key field empty, and as long as the instance only listens on 127.0.0.1 that is safe for local testing. The moment you expose it to the LAN or share the endpoint, go to Settings → Auth & Info and set a long random string. Current versions also offer sub-keys that can only call the API and cannot log into the panel or change settings, meant for handing access to more than one application.
Can I use Claude Code from a MacBook against oMLX running on a Mac Studio?
Yes, through SSH port forwarding: ssh -L 8000:localhost:8000 user@mac-studio makes 127.0.0.1:8000 on the laptop point at the Studio's oMLX. Then export the same variables (ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN and the ANTHROPIC_DEFAULT_*_MODEL set) in a script such as claude-local.sh, make it executable with chmod +x and run claude --bare from the SSH session. If you set a key in Settings → Auth & Info, replace the empty string with it.
Are the benchmarks in this post still valid if I install 0.6.4?
The May tables, no: they were measured on 0.3.8, and when I re-ran them on 15 September 2026, 0.6.4 was faster on this same machine. Without acceleration, Qwen3.6-35B-A3B 8-bit goes from 15.2 to 77.9 tok/s with a 32k prompt. Lightning MTP or VLM MTP add between 1.07x and 1.88x, depending on the model and the context. If you measure on your own Mac, compare through the API and not only with Bench → Performance, which runs vision models on a different engine when MTP is off.
---
Originally published on jacar.es, where the benchmarks are kept up to date with each oMLX release.
Qwen3.8 vs Ornith 1.5 on a Mac: speed and code review
On a Mac Studio M4 Max with 64 GB, Qwen3.8-27B with MTP does not replace the 35B mixture-of-experts model I already run. Measured on 12 October 2026 under oMLX 0.7.0, Qwen generated 55 tokens per second against 90, and took 3.46 seconds to produce its first token on an 800-token prompt against 0.49. On a code review test the two caught the same bugs with reasoning off, and Qwen caught fewer with it on.
The number that decides it is time to first token
Qwen3.8-27B needs seven times longer than Ornith 1.5 to start answering an 800-token prompt: 3.46 seconds against 0.49, averaged over 8 requests per model. Output speed is closer, 55.1 tokens per second against 90.4. In a chat that gap is a nuisance. A coding agent or a code review sends long prompts many times in a row, and there the wait before the first token is most of the cost.
oMLX is an inference server for Apple Silicon Macs that serves MLX models behind an OpenAI-compatible API. I run both models on it, and each one has its own trick for generating faster.
Qwen3.8-27B is a dense model: all 27 billion parameters work on every token. It ships with MTP, multi-token prediction, an extra head trained with the model that drafts the next few tokens so the model can verify them in one pass. Ornith 1.5 35B-A3B is a mixture of experts: 35 billion parameters, about 3 billion active per token. I run it with DFlash, where a separate small draft model proposes a block of tokens and the main model checks them.
The chart puts both measurements side by side. The first pair of bars is the one I care about.
!Bar chart of two measurements on oMLX 0.7.0. Time to first token on an 800-token prompt: Ornith with DFlash 0.49 seconds, Qwen3.8-27B with MTP 3.46 seconds. Output speed: Ornith 90.4 tokens per second, Qwen 55.1. Ornith wins both.
I measured with aiperf 0.12.0, one model loaded at a time, the server restarted before each run and no other traffic. This is the command. It works against any OpenAI-compatible server:
aiperf profile --url http://localhost:8010 --model <model-id> \
--endpoint-type chat --streaming \
--concurrency 1 --request-count 8 --num-warmup-requests 2 \
--prompt-input-tokens-mean 800 --prompt-input-tokens-stddev 0 \
--output-tokens-mean 400 --output-tokens-stddev 0 \
--extra-inputs '{"temperature":0.6,"chat_template_kwargs":{"enable_thinking":false}}' \
--tokenizer <model-snapshot-dir> --random-seed 7
Upgrading oMLX helped Qwen by about 20%, and it was not enough
Moving from oMLX 0.6.4 to 0.7.0 raised Qwen3.8-27B from 57-59 to 71.7 tokens per second on a code prompt and from 46-48 to 55-56 on prose, two runs per cell with 400 output tokens. The oMLX 0.7.0 release notes list faster MTP verify and decode for this model, and the numbers agree. Ornith stayed where it was: 103-121 tokens per second on code, 99-102 on prose.
I also tried the obvious knob. MTP drafts up to 4 tokens per cycle by default on this model, and oMLX lets you raise the ceiling. At 6 the output speed was 53.9 tokens per second, at 8 it was 52.4, against 55.1 at the default. The adaptive controller already picks a depth that pays, so I put the setting back.
On a code review test the two models tie
With reasoning off, Qwen3.8-27B caught 9 of 20 planted bugs and Ornith 1.5 caught 8. With 20 cases, one bug of difference is noise. The test takes 20 real commits from four of my repositories, flips one operator on an added line (< to <=, == to !=, a dropped not) and asks the model to review the diff in a single call, without tools. Each case is paired with the clean diff, and a model only gets credit if it flags the planted line on the mutant and not on the clean version.
| Model | Reasoning | Bugs caught | Unreadable answers | Findings per clean diff | Median time |
|---|---|---|---|---|---|
| Qwen3.8-27B + MTP | off | 9 of 20 (45%) | 4 of 40 | 0.7 | 10 s |
| Ornith 1.5 + DFlash | off | 8 of 20 (40%) | 2 of 40 | 1.6 | 3 s |
| Qwen3.8-27B + MTP | on | 6 of 20 (30%) | 12 of 40 | 0.3 | 194 s |
| Ornith 1.5 + DFlash | on | 8 of 20 (40%) | 13 of 40 | 0.0 | 102 s |
The Ornith rows were measured on 2 October 2026 on oMLX 0.6.4, the Qwen rows on 12 October on 0.7.0. With reasoning off, where they differ is false alarms. Qwen reports half as many problems on code that has none, and it takes three times as long per review.
Turning reasoning on does not help Qwen. It caught 6 of 20 against 9 without it, and half of its 40 answers used up the whole 12,000-token output limit, which is why 12 of them never reached valid JSON. Ornith overthinks in the same way and loses 13 of 40 answers, but it holds at 8 of 20 and takes about half the time per review. A review that needs three minutes and fails to parse a third of the time is no use to me from either model.
The two models do not fit in memory together
Qwen3.8-27B takes 16.1 GB once loaded and Ornith 1.5 takes 19.8 GB. On a 64 GB machine that sounds fine, and under oMLX 0.6.4 both did load, with swap climbing to 15.3 of 16 GB. The memory guard in 0.7.0 refuses instead. While Qwen was busy, every request for Ornith got this:
POST /v1/responses → 507: Cannot load Ornith-1.5-35B-A3B-MLX-4bit: projected memory
38.24GB would exceed the dynamic memory ceiling 34.51GB
When Qwen is idle, oMLX evicts one model to load the other, and the log marks the unload as an immediate abort, so a request in flight on the evicted model is lost. I found out what that looks like under load by accident. I ran the review test on Qwen with reasoning on for 90 minutes while another tool kept sending requests to Ornith. oMLX loaded Ornith, evicted it six seconds later to make room for Qwen's prompt, loaded it again, and eventually evicted Qwen in the middle of an answer. Of Qwen's 40 requests, 19 failed.
I had hoped to keep Qwen beside Ornith as a second opinion, and that is off the table. I have to pick one, and on these numbers I pick Ornith.
Qwen sometimes cuts its own JSON, and the fix costs half the speed
In 5 of 40 review answers Qwen3.8-27B returned {"findings":[] and stopped, with the closing brace missing. It only happened on empty answers of five or six tokens. I reran the five affected diffs three times each under four settings, with MTP on and off and with the presence penalty at 1.5 and 0. The cut answers appeared with MTP off too, and without the penalty, in up to 4 of 15 runs. As far as I can tell the model just ends early now and then.
Constrained decoding removes it. oMLX has it built in through xgrammar, but the Homebrew formula leaves it out unless you ask:
brew reinstall jundot/omlx/omlx --with-grammar
With a JSON schema in the request, 0 of 30 answers were unparseable. The diagram shows what that costs. A grammar and MTP cannot run together, so those requests fall back to plain decoding at about 25 tokens per second.
!Flow diagram of a review request to Qwen3.8-27B. Without a JSON schema it uses MTP decoding at about 55 tokens per second and up to 4 of 15 answers are unparseable. With a JSON schema it uses grammar-constrained decoding with MTP switched off at about 25 tokens per second and 0 of 30 answers are unparseable.
What is still open
Qwen's row with reasoning on mixes two runs. 21 answers come from the morning run described above, with other traffic on the server, and I repeated the 19 failed requests in the afternoon with everything else paused. I counted the morning answers because they were complete, but I did not rerun all 40 in one clean pass. The speed test used 8 requests and one prompt size. I did not test Qwen's vision, and I do not know what a JSON schema costs Ornith with DFlash.
I removed Qwen from the proxy that evening and deleted its weights, so none of that will be measured. The next candidate is Muse Glimmer 30B, which is being measured now.
For interactive work, where you wait on every answer, Ornith stays. If you are choosing a local model for a Mac, measure time to first token at the prompt length you really send before you look at tokens per second. For unattended jobs that gap matters much less.
Mentions
Creating Custom AI Models on a Macbook Pro AI has changed the way we work, whether we’re developers or not. Understanding how LLMs work behind the scenes is a good skill to have, and it also helps you work with AI chat prompts in much more precise and useful ways. What Are LLMs, Fine-tuning & RAG? Many of the models used by popular tools like ChatGPT, Claude, and DeepSeek are actually available as open-source at huggingface.com. We can use these as a base model and supply them with specific training data to fine-tune the model to be good at specific tasks. Different models are good for different things, much like the way we work. I’d expect a plumber to be great at fixing a leaking water main, but I wouldn’t expect them to be great at handling my taxes. The same goes for models, although the training data each uses isn’t quite as drastically different at this point. In the future, I do think highly specialized models will be the norm. If you’ve ever had an LLM lie to you confidently, you’ve experienced an AI concept known as a hallucination. AI hallucinations happen when an LLM makes up information that sounds real but isn’t true. It’s like when the computer is trying to answer a question but doesn’t actually know the answer, so it guesses and tries to sound confident about its guess. Just like how a person might make up a story that sounds believable when they don’t know the real facts, AI can do the same thing without meaning to lie. It just doesn’t know the difference between what’s real and what it’s making up. If you’ve experienced this, then you’ve found a great case for fine-tuning your own model. As a programmer, it’s common to have an LLM do this when you’re asking specific questions about something. I had it happen this week while generating some fish CLI scripts. Claude just didn’t have the specialized knowledge to answer properly. In circumstances like this, it would be great to fill that gap yourself. If you could just give the AI bot the missing information, you know it could work. That’s what fine-tuning provides. With fine-tuning, you’re adjusting its parameters using additional training on specific data to make it better at particular tasks or domains. This process takes a pre-trained model and teaches it to respond in ways that are more aligned with your specific needs, whether that’s adopting a certain writing style, following particular instructions, or becoming more knowledgeable about specialized topics. RAG, or retrieval-augmented generation, is another way to provide data to an LLM, but it’s different from fine-tuning. RAG retrieves and injects relevant information at inference time rather than permanently modifying the model ahead of time with training. With RAG, you search external databases or documents for relevant context and feed that information to the model alongside your prompt, while fine-tuning actually changes how the model behaves by updating its internal parameters through additional training. Why Create a Custom Writing Assistant? There are many circumstances where fine-tuning your own custom model makes a lot of sense, and creating a ghostwriter with your own voice is a simple and effective way to learn how to do it. Let’s explore a real-world use case to understand how all this works. I’ve been writing blog posts at Kevinleary.net since 2009, and I continue to write today (that’s obvious). I use AI to assist with research for writing and to help correct grammar and spelling, but generating full posts with tools like ChatGPT or Claude never captures my voice. I can tweak and adjust, but I always end up rewriting a lot myself. But what if I used all of my blog posts as data to fine-tune an LLM base model, like Llama 3.1 8B? Doing this will provide me with a specialized LLM trained on 10 years of my own writing style. The likelihood it will be able to match my own voice and write full blog posts for me is substantially higher. So let’s do it. Formatting Data for AI Fine-tuning The first step in the process is to get our data and convert it to a format that can be used to fine-tune our model. For WordPress, the WP CLI is a fast, effective way to do this. We’ll use it to query my posts and convert them to JSONL, which is really a fancy way of saying this is JSON with one record per line. To do this, I’ve used the following bash script, which allows me to create two JSONL files for fine-tuning. I ran this in my localhost after syncing my live site to include all articles; I recommend you do the same. if (isset($_GET['export']) && in_array($_GET['export'], ['train', 'valid'])) { global $wpdb; $posts = $wpdb->get_results(" SELECT p.post_title, p.post_date, p.post_content_filtered, pm.meta_value as lead_in FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = 'extras_lead-in' WHERE p.post_status = 'publish' AND p.post_type = 'post' AND p.post_content_filtered != '' ORDER BY p.post_date DESC "); $total = count($posts); $train_count = floor($total * 0.8); if ($_GET['export'] === 'train') { $filtered_posts = array_slice($posts, 0, $train_count); $filename = 'train.jsonl'; } else { $filtered_posts = array_slice($posts, $train_count); $filename = 'valid.jsonl'; } header('Content-Type: application/json'); header("Content-Disposition: attachment; filename=\"{$filename}\""); foreach ($filtered_posts as $post) { $date = date('F j, Y', strtotime($post->post_date)); $title_with_lead = $post->post_title . (!empty($post->lead_in) ? ' - ' . $post->lead_in : ''); $jsonl = json_encode([ 'messages' => [ [ 'role' => 'user', 'content' => "write a blog post in markdown for kevinleary.net entitled \"{$title_with_lead}\" as if it were originally published on {$date}", ], [ 'role' => 'assistant', 'content' => $post->post_content_filtered, ], ], ], JSON_UNESCAPED_UNICODE); echo $jsonl . "\n"; } exit; } Once this is in place, we can generate the two files we need for fine-tuning by visiting your site with the following URL parameters added to the URL: - ?export=train - ?export=valid Each will prompt you to save a .jsonl file to your system, which becomes our training data for fine-tuning. In this case, it formats my WordPress posts, which are written in markdown, into the following JSON data structure: { "messages": [ { "role": "user", "content": "write a blog post in markdown for kevinleary.net entitled '{TITLE} - {TEASER / LEAD-IN}' as if it were originally published on August 10, 2025" }, { "role": "assistant", "content": "{MARKDOWN CONTENT}" } ] } We’ll use this as our training data in the fine-tuning process. Setting Up the Training Environment To train our AI model, we’ll use a framework specifically built for Apple Silicon called MLX, which runs on Python, so we’ll need to install that if you don’t already have it. Using the package installer provided by Python is the best way I’ve found. Once you’ve installed Python, install MLX with pip: pip install mlx-lm The first step is to download a local copy of the model we’ll be training, then feed our JSONL to it to create our custom model. I’ve saved mine into a ~/AI/ directory on my Mac. Running the Fine-tuning Process We can use it to start the fine-tuning process with a single command: mlx_lm.lora --model mlx-community/Meta-Llama-3.1-8B-Instruct-4bit --data . --train --batch-size 1 --num-layers 16 --iters 600 --learning-rate 1e-5 --save-every 100 When the training process completes, we’re left with an ./adaptors directory containing adaptor files. These are the files that we can use to create a CLI or UI to work with our model. Using the Custom LLM Once we have our completed adaptors as output from the training process, we can use the model in the command line: mlx_lm.generate --model mlx-community/Meta-Llama-3.1-8B-Instruct-4bit --adapter-path adapters.npz --prompt "Write a blog post for kevinleary.net about JavaScript performance" That’s a mouthful; it’s best to simplify with a custom CLI function. I use Fish shell, so I’ve created a helpful fish function to make it better: Fish CLI Function function kevinlearynet-writer mlx_lm.generate \ --model mlx-community/Meta-Llama-3.1-8B-Instruct-4bit \ --adapter-path /Users/kevinleary/AI/adapters/ \ --prompt $argv \ --max-tokens 4000 \ --temp 0.8 \ --top-p 0.9 \ --min-p 0.05 end Bash Function If you use Bash as a shell you can add this to your .bash_profile and run the same command to generate content. function kevinlearynet-writer() { mlx_lm.generate \ --model mlx-community/Meta-Llama-3.1-8B-Instruct-4bit \ --adapter-path /Users/kevinleary/AI/adapters/ \ --prompt "$*" \ --max-tokens 4000 \ --temp 0.8 \ --top-p 0.9 \ --min-p 0.05 } Now I can use my custom AI model in the Terminal whenever I want like this: kevinlearynet-writer "Write a blog post for kevinleary.net about JavaScript performance" Conclusion You should have a pretty good idea of how to create a custom LLM by fine-tuning an open-source model from huggingface.com. In this example, we’re creating a writing assistant that can match my voice and writing style by training on blog posts from Kevin Leary.net. If you’re a business that has done content marketing and has a fair amount of content written, you can do the same, and if you’re using WordPress, you can use the exact same methods for pulling the data out, but the same process could be used with any CMS, just using different methods. This is only one example of how to use a custom LLM to improve your workflow over out-of-the-box tools like ChatGPT and Claude, but the general process can be used for many different applications. You provide data, fine-tune with training, and then you get a more specialized LLM that does what you want with more accuracy and less hallucination. Creating a custom fine-tuned LLM provides a superior approach to working with AI for specialized situations such as debugging code in your company’s specific framework, generating marketing copy that matches your brand voice, or providing technical support answers using your product’s exact API documentation and error messages. If you’re looking for a good AI consultant to help with training custom models, give me a shout.
@dealignai Pretty sure there is solid evidence that prefill is indeed quite a bit faster for 256gb+ models. See GLM 5.1 benchmarks earlier this year on similar M3 configs: mlx#3402. See comment by @angeloskath. Happy to be proven wrong, as I’m interested in this myself.
Some recent awesome optimizations merged in MLX: Built-in sliding window support in fused SDPA mlx#4734 Use mma/nax for QMV with small rows and large N/K, making batched decoding 2~3x faster mlx#4712 Dequantize fp4 into float16 directly, which makes prefilling 1.4x faster for old macs mlx#4709 Add a 2-stage nax QMM kernel that makes small prompt prefill almost 2x faster for some models mlx#4682 1-bit affine quantization mlx#3161 Thin GEMM kernels making small-M/large-N and large-M/small-N 1.5x faster mlx#4640 1.4x faster fused SDPA by prefetching K/V mlx#4596 And there are actually a lot more awesome changes that I'm too exhausted to list them all, the commit history of MLX is an interesting read for anyone who wants to make open source contributions. github.com/ml-explore/mlx/commits/main
Квантование RWKV-7: что ломается в моделях разного размера 1. Введение Модели семейства RWKV с линейной по длине последовательности рекуррентной структурой привлекательны для локального запуска, но инструментов квантования для них заметно меньше, чем для трансформеров. В этой работе я описываю инструмент rwkv-quant, сжимающий RWKV-7 двумя готовыми пресетами, и систематически разбираю, где именно при квантовании теряется качество и как это зависит от размера модели. Работа началась с наблюдения, которое противоречит обычной интуиции «чем больше модель, тем меньше относительная потеря». Пресет COMPRESSION на 2.9B показал +4.04% по перплексии против +2.85% на 1.5B. Выяснение причины заняло основную часть исследования и привело к набору результатов, которые я считаю полезными независимо от конкретного инструмента. Вклад работы. Локализация потерь. Разложение KL по группам матриц (leave-one-out) показывает, что на 2.9B вред сосредоточен в двух местах: выходной проекции нулевого блока (o_proj слоя 0) и таблице эмбеддинга. Хранение первой в bf16 стоит 0.5% файла и снимает 39% KL; перевод второй с 5 на 6 бит стоит 1.1% файла и снимает ещё 22% (раздел 4). Механизм и предсказуемость. Вход проблемной матрицы несёт массивный канал, но сама концентрация энергии вред не предсказывает: у 7.2B она выражена сильнее всех, а модель квантуется лучше всех. Я показываю, что три естественные локальные метрики ошибки не работают, и описываю метрику, которая работает: ошибка в остаточном потоке, умноженная на измеренное усиление (раздел 5). Автоподбор битности по правилу «вред на байт», работающий в обе стороны (одна цена байта для повышения и понижения), с измерением по одной матрице за проход (раздел 6). GPTQ на сетке пресета без изменения формата, ядер и размера файла; показано, что регуляризация важнее размера калибровки и что методы автоподбора и GPTQ складываются почти мультипликативно (раздел 7). GPTQ в пресете REDUCTION описан в разделе 8. Сводка по шести размерам от 0.1B до 13.3B с доверительными интервалами, полученными парным бутстрэпом по окнам (раздел 9), и сравнение с GGUF и MLX при равном размере файла (раздел 10). Методологические находки. Утечка калибровочного корпуса в оценочный набор и её измеренный масштаб (раздел 3.4); разброс результата GPTQ между численно эквивалентными реализациями и его количественная оценка (раздел 7.6); четыре инженерные ошибки, исказившие измерения (приложение A). 2. Связанные работы Квантование с коррекцией ошибки. GPTQ (Frantar et al., 2022) квантует колонки матрицы по одной и разносит ошибку каждой на ещё не квантованные колонки через обратный гессиан входных активаций. Я переношу GPTQ на асимметричную суперблочную сетку (раздел 7) и симметричную сетку типа Q6_K (раздел 8). Формат файла и ядра при этом не меняются. Взвешивание по активациям. AWQ и матрица важности imatrix в llama.cpp используют статистики активаций, чтобы защитить более важные каналы. В моём инструменте статистика активаций (AW, activation-weighted) определяет выбор масштаба и сдвига групп весов; без неё 4 бита в o_proj разрушают 2.9B (раздел 4.3). Выбросы и массивные активации. Dettmers et al. (2022, LLM.int8()) описали выбросы признаков, концентрированные в нескольких каналах скрытого состояния, и их роль в деградации квантованных трансформеров. Sun et al. (2024) описали «массивные активации» — значения на порядки больше медианных в немногих каналах. Массивный канал на входе o_proj нулевого блока (раздел 4.3) — прямая родня этих явлений, но, как я покажу, его сила сама по себе не предсказывает вред. 3. Постановка и методика 3.1 Модели Исследуется линейка RWKV-7 шести размеров. Для изоляции факторов использованы три пары: 0.4B/1.5B (одна глубина, ширина вдвое больше), 2.9B/7.2B (одна глубина L=32, ширина 2560 против 4096) и 7.2B/13.3B (одна ширина, глубина 32 против 61). модель слоёв L ширина D голов H=D/64 D/L серии чекпоинтов 0.1B 12 768 12 64 g1d 0.4B 24 1024 16 42.7 g1d 1.5B 24 2048 32 85.3 g1d, g1h, g1j 2.9B 32 2560 40 80 g1h, g1j 7.2B 32 4096 64 128 g1j 13.3B 61 4096 64 67.2 g1j Итоговые таблицы (разделы 9–10) относятся к серии g1j; диагностика (раздел 4) частично использует g1d и g1h, это указано в тексте. 3.2 Пресеты Инструмент rwkv-quant сжимает модель двумя пресетами. REDUCTION (почти без потерь). Проекции (proj) — 8 бит, симметричная раскладка sym_aw с группой 16 весов; cmix (каналовое микширование, FFN) — 6 бит; эмбеддинг и голова — 8 бит; LoRA-ветки w, a, v — 6 бит, g — 8 бит (блок 64). Сжатие около 2.1x. COMPRESSION (агрессивный). Проекции — 4 бита, асимметричная суперблочная раскладка asym_sb6_aw с группой 32; cmix — 4 бита, но ffn.key — 5 бит; эмбеддинг — 6 бит (в ранних измерениях 5, раздел 4.5); голова — 5 бит; LoRA-ветки те же, что в REDUCTION. Сжатие около 3.1x. В раскладках с суффиксом _aw масштаб и сдвиг каждой группы подбираются поиском, взвешенным по среднему квадрату входной активации канала E[x²], то есть по статистике, снятой на калибровочном корпусе. Раскладка REDUCTION симметричная (масштаб на блок из 16 весов, без сдвига; int8-коды масштабов против одного fp16-множителя на суперблок из 16 блоков; 6 или 8 бит). Раскладка COMPRESSION (sb6) асимметричная суперблочная: блок из 32 весов, суперблок из 8 блоков (256 колонок); масштаб и сдвиг каждого блока хранятся 6-битными кодами qs и qm относительно двух fp16-множителей суперблока. Не квантуются и хранятся в bf16 малые поканальные векторы (k_k, k_a, r_k), нормы и bias LoRA-веток. У таблицы эмбеддинга статистики активаций нет (её вход — индексы токенов), поэтому её сетка в обоих пресетах подбирается без AW-взвешивания; GPTQ эмбеддинг также не затрагивает. Две правки, найденные в разделе 4, входят в финальный COMPRESSION: выходная проекция нулевого слоя (o_proj слоя 0) хранится в bf16, эмбеддинг — в 6 битах. Пресет без этих правок в тексте называется «базовым» (в логах compression_nol0). Рис. 1. Конвейер quantize(): от чекпоинта bf16 до файла .rwkvq Рис. 1. Конвейер quantize(). Статистика активаций E[x²] используется автоподбором, GPTQ и записью; результат автоподбора дописывается в конфиг (bits_overrides). Калибровка не зависит от платформы; формат и бэкенд отделены от неё. Рис. 2. Битность каждой матрицы блока RWKV-7 в двух пресетах Рис. 2. Битность матриц в пресетах REDUCTION и COMPRESSION. Выделены две фиксированные правки: o_proj слоя 0 в bf16 и эмбеддинг на 6 бит вместо 5 (раздел 4). Автоподбор (раздел 6) меняет биты отдельных матриц поверх пресета COMPRESSION. 3.3 Метрики и приборы Основная метрика — KL(bf16‖квант) на токен: расхождение распределения следующего токена у квантованной модели от распределения bf16-модели. Эталонные логиты считаются в fp32 и сохраняются на диск (5.09 ГБ на модель). Вторая метрика — относительное изменение перплексии (Δppl, в процентах) к bf16-модели. Эталон принимается числом (оценка эталонного плеча), а не размером файла. Используются два пути вычислений, и их абсолютные KL различаются, поэтому сравнивать можно только внутри одного прибора: fake-путь (PyTorch): веса квантуются и тут же деквантуются, вычисления в плотных тензорах; работает на Mac (MPS) и на сервере (CUDA); реальный путь (MLX): коды упакованы в формат .rwkvq и читаются Metal-ядрами; только Mac. Приборы перечислены в приложении C. 3.4 Оценочные наборы и утечка Первый набор eval_corpus_multiling (en/ru/sr, 38 окон по 512 токенов) использовался в разделах 4–6 и оказался целиком лежащим внутри калибровочного корпуса пакета: общих 16-грамм 100%. Для метода, калибруемого по корпусу, это прямая утечка. Масштаб утечки измерен на GPTQ (раздел 7), откалиброванном по корпусу пакета: на 1.5B выигрыш по KL текста составил −61.8% на старом наборе и −27.1% на чистом, то есть утечка завысила выигрыш в 2.3 раза. Код чист (одно окно из 24 содержит общие 16-граммы, 0.03%). AW-статистика задета частично: её 23 окна по 512 токенов содержат 23.9% 16-грамм старого набора и пересекаются с 13 из 38 его окон. Абсолютные числа раздела 4 получены на тексте, по которому AW частично взвешивала поиск; направление смещения в пользу AW, величина не измерялась. Сравнения внутри раздела (какая матрица, какой бит) сделаны при одной и той же статистике, и смещение входит в оба плеча. Автоподбор утечкой не воспользовался: на 1.5B −16.1% на старом наборе и −17.6% на чистом. Чистые наборы (ни одной общей 16-граммы ни с одним калибровочным корпусом): eval_text_heldout: Википедия en/ru/sr, по 12 окон на язык (36 окон); eval_code_heldout: 24 окна Python/Swift (закрытый набор); eval_code_open: 48 окон (24 Python, 24 Swift) из открытых репозиториев автора. Правило: любой калиброванный метод мерится только на чистых наборах, и перед замером проверяется, что пересечение 16-грамм калибровки с набором равно нулю. Разделы 4–5 и таблица выбора порога в разделе 6.3 измерены на старом наборе (в таблице раздела 6.3 окна не участвовали в выборе, но лежат внутри калибровочного корпуса); результат автоподбора перепроверен на чистых наборах в разделе 6.4. Разделы 7–10 используют только чистые наборы. 3.5 Статистика Доверительные интервалы строятся парным бутстрэпом по окнам (20 000 повторов, окна ресэмплируются вместе для обоих сравниваемых плеч). При сравнении с чужими форматами то же самое делается для отношений KL. У KL полуширина 95%-интервала составляет 7–12% от значения. Дополнительно, для оценки разброса самого GPTQ между реализациями, использованы реплики с переставленным порядком окон калибровки (раздел 7.6). 4. Где теряется качество Замечание о наборах. Измерения разделов 4 и 5 сделаны на первом оценочном наборе (раздел 3.4), частично пересекающемся с AW-статистикой. Абсолютные значения могут быть слегка смещены в пользу AW; сравнения между матрицами и битностями внутри раздела сделаны при одной статистике. Все дальнейшие результаты (разделы 6.4–10) получены на чистых наборах уже с найденными здесь правками в пресете. Базовый вопрос: почему COMPRESSION на 2.9B деградирует сильнее, чем на 1.5B (+4.04% против +2.85% по ppl)? 4.1 Разложение потерь по группам матриц Вклад группы матриц определяется методом leave-one-out: вклад = KL(всё квантовано) − KL(всё, кроме этой группы). Результаты для серии g1h, реальный путь (Mac, MLX): группа 1.5B: KL (доля) 1.5B: ppl, п.п. 2.9B: KL (доля) 2.9B: ppl, п.п. o_proj 0.00578 (16%) 0.64 0.02368 (46%) 1.82 cmix.value 0.01317 (37%) 0.99 0.01035 (20%) 0.63 эмбеддинг 0.00120 (3%) 0.07 0.00874 (17%) 0.83 cmix.key 0.00442 (12%) 0.43 0.00400 (8%) 0.36 голова 0.00440 (12%) 0.39 0.00310 (6%) 0.36 att.value 0.00443 (12%) 0.37 0.00378 (7%) 0.52 r, k, LoRA ≤4% ≤0.08 ≤2% ≤0.12 Сумма вкладов равна 101% и 107%, то есть группы почти аддитивны. На 2.9B выделяются две группы: o_proj (46% всего KL) и эмбеддинг (17%); на 1.5B их вклад скромный (16% и 3%), главный вклад даёт cmix.value. Тот же разбор на сервере (fake-путь, серия g1j, базовый пресет compression_nol0) подтверждает картину: модель базовый KL o_proj слоя 0 весь o_proj эмбеддинг cmix.value голова 1.5B 0.050962 0.5% 13% 2.8% 29% 8.3% 2.9B 0.077513 39% 45% 30% — — 7.2B 0.022984 3.9% 13% 8.4% 25% 9.4% Рис. 3. Доли потерь KL по группам матриц на 1.5B и 2.9B Рис. 3. Доли потерь KL по группам матриц (leave-one-out, реальный путь, серия g1h), 1.5B и 2.9B. Данные первой таблицы раздела. 4.2 Главная находка: o_proj нулевого слоя На 2.9B 85–86% вклада всей группы o_proj приходится на одну матрицу: выходную проекцию нулевого блока. Прямой эксперимент (реальный путь, 2.9B, g1h) сравнивает базовый пресет с его вариантами, в которых отдельные части o_proj хранятся с большей точностью: плечо KL ppl en / ru / sr прирост файла базовый 0.052048 +4.04% +4.24 / +3.21 / +5.71 — o_proj слоя 0 в bf16 0.031966 +2.43% +3.02 / +2.19 / +2.36 +9.4 МБ (+0.5%) o_proj слоя 31 в bf16 0.051512 +4.07% ≈ базовый +9.4 МБ весь o_proj в 5 бит 0.046043 +3.54% ru хуже: 3.21→3.44 +26.2 МБ весь o_proj в 6 бит 0.036305 +2.89% — +52.4 МБ Одна матрица 2560×2560 в bf16 возвращает 85% вклада группы o_proj за 0.5% файла и работает лучше, чем перевод всего o_proj в 6 бит за 2.8% файла. Контрольное плечо (слой 31) эффекта не даёт, то есть дело именно в нулевом слое. После правки 2.9B становится лучше 1.5B: +2.43% против +2.85%. Та же правка на других масштабах (реальный путь): пресет модель KL: базовый → с правкой ppl прирост файла COMPRESSION 0.1B 0.08960 → 0.08279 (−7.6%) +8.68% → +8.01% +0.8 МБ COMPRESSION 0.4B 0.04965 → 0.04891 (−1.5%) +4.31% → +4.28% +1.5 МБ COMPRESSION 1.5B 0.03599 → 0.03585 (−0.4%) +2.85% → +2.87% +6.0 МБ COMPRESSION 2.9B 0.05205 → 0.03197 (−39%) +4.04% → +2.43% +9.4 МБ REDUCTION 0.4B / 1.5B без изменений — +1.0 / +3.9 МБ REDUCTION 2.9B 0.001635 → 0.001573 (−3.8%) +0.08% → +0.06% +6.1 МБ Значимы только эффекты на 0.1B и 2.9B: на 0.4B и 1.5B изменение кросс-энтропии не отличается от нуля. Поправка существенна лишь в COMPRESSION на 2.9B и, в меньшей степени, на 0.1B. 4.3 Механизм: массивный канал на входе o_proj слоя 0 Вход o_proj нулевого слоя несёт «массивный канал»: один-два канала на несколько порядков сильнее остальных по среднему квадрату активации E[x²]. модель max/медиана E[x²] доля энергии в 8 каналах номер канала 0.1B g1d 2 628 0.77 112 0.4B g1d 22 580 0.91 53 1.5B g1d 2 245 0.46 1202 1.5B g1h 3 740 0.53 1202 1.5B g1j 4 227 0.54 1202 2.9B g1h 7 199 995 0.98 517 2.9B g1j 7 564 462 0.98 517 7.2B g1j 6 626 554 0.99 160 Номер канала один и тот же у всех серий одной ширины: 1202 у трёх серий 1.5B, 517 у двух серий 2.9B. Следовательно, это свойство ширины и архитектуры, а не конкретного прогона обучения. Насколько AW-статистика держит эту матрицу, видно из плеча «o4s»: те же 4 бита, но поиск без AW, только в o_proj. модель KL: базовый → без AW ppl 1.5B 0.035992 → 0.042974 +2.85% → +3.57% 2.9B 0.052048 → 0.340025 +4.04% → +37.2% На 2.9B AW держит почти всё: без неё 4 бита в o_proj разрушают модель. 4.4 Почему именно 2.9B, а не «все большие модели» Ожидание «больше модель — меньше деградация» в целом верно: на 2.9B все обычные группы (r, k, v, cmix, голова, LoRA) уносят меньше, чем на 1.5B, даже в абсолютных числах. Выбиваются только o_proj и эмбеддинг. Однако на 7.2B массивный канал выражен сильнее всех (99% энергии в восьми каналах), а квантуется эта модель лучше всех: базовый KL 0.023 против 0.078 у 2.9B и 0.051 у 1.5B (одна серия g1j, один прибор), и правка слоя 0 даёт там лишь −3.9%. Значит, сила массивного канала сама по себе не объясняет вред. Прямой замер вреда от квантования одной матрицы (квантуется только o_proj слоя 0, остальное в bf16) на 2.9B и 7.2B: KL ‖Δx‖/‖x‖ на слое 0 пик по глубине 2.9B 0.020290 13% 31% (слои 13–16) 7.2B 0.001427 3.4% 6.7% (слой 13) Разница в 14 раз по KL создаётся в момент впрыска ошибки в остаточный поток, а не при её распространении: усиление вниз по глубине у обеих моделей близкое (2.4x и 2.0x к середине сети). Это подводит к вопросу, чем вред вообще предсказывается (раздел 5). Рис. 4. Сила массивного канала и вред от квантования o_proj слоя 0 Рис. 4. Сила массивного канала (max/медиана E[x²]; серии g1j) и вред от квантования только o_proj слоя 0 на 2.9B и 7.2B. Сила канала почти одинакова, вред различается в 14 раз. 4.5 Второй адрес: эмбеддинг На 2.9B эмбеддинг уносит 17% KL (серия g1h) и 30% (g1j), на 1.5B — 3%. Перевод эмбеддинга из 5 бит в 6 (реальный путь, поверх пресета с правкой слоя 0): модель KL ppl en / ru / sr прирост файла 2.9B, базовый 0.031966 +2.43% +3.02 / +2.19 / +2.36 — 2.9B, эмбеддинг в 6 бит 0.024885 (−22%) +1.86% +2.33 / +1.66 / +1.82 +21.0 МБ (+1.13%) 2.9B, эмбеддинг в bf16 0.024539 (−23%) +1.82% — +220 МБ 1.5B, эмбеддинг в 6 бит 0.034933 (−2.6%) +2.82% шум +16.8 МБ 6 бит забирают 95% того, что даёт полный bf16, в десять раз дешевле. Восемь бит для эмбеддинга недоступны: суперблочная раскладка знает только 4, 5 и 6 бит. Обе правки вместе переводят 2.9B с +4.04% до +1.86% ценой примерно 1.6% файла. Решение: эмбеддинг в 6 бит входит в COMPRESSION всегда. Пересчёт по записанным файлам (реальный путь, 38 окон, прежний файл меряется тем же прибором рядом): модель ppl: было → стало KL: было → стало ΔCE, 95% ДИ прирост файла 0.1B +8.00% → +7.50% 0.0828 → 0.0780 значимо +6.3 МБ (+4.9%) 0.4B +4.28% → +4.20% 0.0489 → 0.0468 в шуме +8.4 МБ (+2.9%) 1.5B +2.87% → +2.82% 0.0359 → 0.0349 в шуме +16.8 МБ (+1.7%) 2.9B +2.43% → +1.85% 0.0320 → 0.0249 значимо +21.0 МБ (+1.1%) Цена правки — только байты файла: в COMPRESSION эмбеддинг разворачивается в плотный fp16 (gather применим лишь к симметричной раскладке), и на каждый токен читается одна строка таблицы. 4.6 Скорость Скорость декода 2.9B измерена на двух файлах, собранных одним кодом и отличающихся только слоем 0 (bf16 против квантованной матрицы), с чередованием ABBA в одном процессе: порядок загрузки разница файл без правки первым −0.40 мс/ток (−1.6%) файл с правкой первым −0.52 мс/ток (−2.2%) контроль A/A −0.02 [−0.13; +0.10] Файл с bf16-матрицей не медленнее, а быстрее, хотя читает на 9.4 МБ больше. Механизм не выяснен: одна матрица не должна давать полмиллисекунды, и я не утверждаю объяснения. Вывод, который из этого следует практически: правка слоя 0 не стоит скорости. 5. Что предсказывает вред от квантования матрицы Раздел 4 показывает, что вред от квантования очень неравномерен между матрицами и что одна матрица с «массивным» входом может быть важнее целой группы. Возникает вопрос, можно ли предсказать такие матрицы заранее, не квантуя каждую по отдельности. Я проверил три естественных признака и все три отвергли. Признак 1: концентрация энергии входа. Доля энергии в восьми каналах не предсказывает вред: 0.99 у 7.2B (вреда практически нет), 0.77 у 0.1B (вред есть), 0.91 у 0.4B (вреда нет). Признак 2: диагональная ошибка выхода слоя У нулевого слоя она наименьшая во всех пяти проверенных моделях. Причина понятна: AW-поиск минимизирует именно эту величину, то есть признак измеряет качество поиска, а не вред. Признак 3: честная локальная ошибка впрыска ‖(W−Ŵ)X‖/‖WX‖ на настоящих активациях. На 2.9B максимум у ffn.value слоёв 15–19 (0.16), а o_proj слоя 0 не попадает даже в первую дюжину. Контроль: квантование только blocks.15.ffn.value даёт KL 0.000776 против 0.020290 у o_proj слоя 0, то есть в 26 раз меньше вреда при вчетверо большей локальной ошибке. Вывод. Локальная ошибка нормирована не на то. Значима ошибка, внесённая в остаточный поток, умноженная на то, насколько последующие слои её усиливают. След ошибки слоя 15 затухает по глубине (4.9% → 2.2%), а след слоя 0 разгоняется (13% → 31%). Признак, который работает. Ошибка подслоя в остаточном потоке ‖Δx‖²/‖x‖², умноженная на усиление G этой точки, где G измерено прямо: шумом ε = 1% в ту же точку и измерением KL на выходе. Ранговая корреляция с истинным одиночным вредом (KL при квантовании только этой матрицы): 0.977 (0.1B), 0.969 (1.5B), 0.920 (2.9B); верхние три матрицы на 2.9B совпадают. Слабости признака: вклад эмбеддинга через точку x₀ переоценён (изотропный шум не похож на настоящую ошибку), а внутренние матрицы слоя 0 недооценены (v_first не моделируется). Для практического правила этот признак оказался не нужен. Правило платит за байт (раздел 6), и для него честнее всего прямой замер одиночного вреда каждой матрицы: один проход на матрицу, что по цене сопоставимо с измерением усиления G (2L+1 проходов). 6. Автоподбор битности: правило «вред на байт» Фиксированный список правок («o_proj слоя 0 в bf16, эмбеддинг в 6 бит») плох тем, что проблемные матрицы у разных моделей разные. Ниже описано правило выбора битности по измерению, работающее в обе стороны. 6.1 Правило Для матрицы m и шага её битности b → b′ задана величина где KL берётся для модели, у которой квантована только эта матрица, против fp32-эталона. Шаг вверх выполняется, если e ≥ τ; шаг вниз — если e < τ. Один порог τ с обеих сторон означает одну цену байта: биты переезжают туда, где окупаются сильнее. Стоимость матрицы в байтах считается как params·(b + 0.5)/8 (0.5 бита на вес — масштабы sb6) и 2·params для bf16. Лестница уровней — 4, 5, 6 бит и bf16. Шаг вниз допустим только до 4 бит (3-битной раскладки в формате нет); эмбеддинг вниз не идёт никогда. Подъёмы выбираются жадно по убыванию e, пока e ≥ τ. Для заданного бюджета файла (по умолчанию +0.5%) берётся наименьшее τ, при котором прирост байт укладывается в бюджет; оно ищется бисекцией по log τ. Если бюджет недостижим даже при самом большом τ, возвращается выбор при этом τ и пометка «недостижим». Цель выбора — средний KL по 14 окнам калибровочного корпуса, взятым по языковой квоте en 4 / code 4 / ru 2 / sr 2 / zh 2 (веса языков пропорциональны числу окон; при необходимости веса можно задать явно). 6.2 Кандидаты и понижение Первая версия ошибалась в выборе кандидатов, а не в признаке. Первоначально проверялись 12 верхних матриц по абсолютному вреду плюс концевые слои. Но правило платит за байт, а мелкие матрицы C×C (att.output и att.value средних и поздних слоёв) при умеренном вреде вчетверо дешевле cmix. Строгая граница снимает вопрос без предположений: шаг вверх не может снять больше одиночного вреда S матрицы, значит По свипу одиночного вреда за пределами 12 кандидатов матриц с границей ≥ 5 оказалось 47 (1.5B), 61 (2.9B) и 59 (7.2B). Полный выбор при τ = 5 — 33 / 46 / 45 ключей вместо 12. Вниз три бита не нужны. Лестница вниз (KL одной матрицы на бит ниже) показала, что шаги на 3 бита при e < 5 дают не более 0.9% файла (7.2B), обычно 0.2–0.6%. Дешёвые байты лежат в другом месте: ffn.key 5 → 4 в средних слоях (e = 1.9–3.5), а 4-битная раскладка в формате уже есть. Новая раскладка не нужна. 6.3 Выбор порога τ Симметричное правило оказывается байт-нейтральным при τ ≈ 5–6: размер пресета был верным, неверным было распределение бит. Ниже — KL файла на 30 окнах, не участвовавших в выборе (но лежащих внутри калибровочного корпуса, раздел 3.4), парно к пресету по окнам; скорость — чередование ABBA в одном процессе на Mac. Δ размера ΔKL, отложенные окна окон хуже Δ декода 1.5B, прежний выбор (12 ключей, только вверх) +22.6 МБ −17.0% [−18.2; −15.8] 0/30 +1.7% 1.5B, sym τ=5 +6.8 МБ −18.8% [−20.3; −17.4] 0/30 в шуме 1.5B, sym τ=6 −2.6 МБ −13.5% [−15.1; −11.7] 1/30 −0.5% 1.5B, sym τ=7 −24.6 МБ −0.8% [−2.8; +1.2] 13/30 −2.0% 2.9B, прежний выбор +55.4 МБ −24.6% [−26.1; −23.1] 0/30 +2.4% 2.9B, sym τ=5 +16.9 МБ −25.7% [−28.4; −22.5] 0/30 в шуме 2.9B, sym τ=6 −8.5 МБ −19.2% [−21.6; −16.6] 0/30 в шуме 2.9B, sym τ=7 −49.2 МБ −6.5% [−9.0; −3.8] 6/30 −2.4% 7.2B, sym τ=5 +86.0 МБ −25.8% [−28.4; −23.6] 0/30 не мерено 7.2B, sym τ=6 −33.6 МБ −13.4% [−16.6; −10.2] 0/30 не мерено 7.2B, sym τ=7 −115.3 МБ −0.7% [−4.3; +2.8] 12/30 не мерено τ = 5 даёт качество прежнего выбора за треть байт и без потери скорости; τ = 7 — качество пресета на файле на 2.5% меньше и на 2% быстрее. Скорость здесь не выводится из байт: прежний выбор добавлял 22.6 МБ и замедлял на 1.7%, а sym τ=5 добавляет 6.8 МБ и не замедляет, потому что байты сместились с матриц, читаемых на каждом токене, на другие. Оговорка, которую данные сделали сами. В этом прототипе выбор делался по 8 окнам (3 ru / 3 en / 2 sr), и английский выигрывает меньше всех (7.2B, τ=6: en −7.7% против sr −19.4%; при τ=7 английский даже на 4.6% хуже пресета). То, что правило минимизирует, — это решение о целевых данных, а не деталь реализации. Рис. 5. Порог τ: размер файла и снятый вред на отложенных окнах Рис. 5. Выбор порога τ: изменение размера файла и KL на 30 отложенных окнах (парно к пресету, 95% ДИ). Данные таблицы выше. 6.4 Чистая оценка на моделях от 1.5B до 13.3B Прибор: fake-путь на сервере, 36 окон чистого текста и 24 окна чистого кода, KL к bf16 тем же прибором (для 1.5B — замер на Mac). Бюджет +0.5% файла. Изменение относительно пресета без автоподбора: модель KL текст en ru sr KL код Δppl текст Δppl код 1.5B (Mac) −17.6% −17.9 −16.9 −18.0 −13.5% +3.88 → +3.25% +3.86 → +3.32% 7.2B −20.7% −25.1 −16.1 −20.3 −13.6% +1.85 → +1.51% +1.22 → +0.86% 13.3B −21.1% −23.4 −17.0 −22.5 −17.4% +1.14 → +0.86% +1.11 → +0.58% С ростом модели выигрыш не падает. Предсказание самого измерения на 13.3B завышено в 1.5 раза: предсказано −32.9%, измерено −21.1% (на 1.5B предсказание было близким). Поэтому, как и выше, результатом считается только замер записанного файла. Полная таблица с парным бутстрэпом для шести моделей приведена в приложении B.1. Её итог: по KL автоподбор значимо лучше пресета на каждой модели и в каждом срезе; по ppl выигрыш не значим в двух клетках (текст на 0.4B и код на 2.9B). Слабое место — английский на 0.4B: KL −7.5%, три окна из двенадцати хуже пресета, ppl +0.46% [−0.25; +1.14]. 6.5 Что выбирает автоподбор Число матриц, у которых бит повышен или понижен (бюджет +0.5% файла): модель вверх вниз что вниз 0.1B 47 1 — 0.4B 52 10 — 1.5B 46 16 — 2.9B 59 25 ffn.key 5 → 4 бита 13.3B 110 37 — Наверх идут в основном o_proj и v_proj (5 бит) и ffn.value. Бюджет +0.5% не выбирается целиком: правило упирается в τ, и прирост файла составляет 0.1–0.2% (на 0.4B размер даже уменьшается). 6.6 Измерение без лестниц и его цена Уровни выше битности пресета измерять не нужно: KL одной матрицы убывает с битом почти одинаково у всех типов матриц и масштабов. Отношение r = KL(b+1)/KL(b) составляет 0.315 (proj), 0.313 (cmix.value), 0.317 (cmix.key) и 0.41 (голова), медианы по 500+ шагам на шести моделях от 0.1B до 13.3B. Выбор по формуле «измеренная база × rᵏ» даёт тот же КПД (процент снятого KL на мегабайт), что выбор по истинным лестницам, на всех проверенных масштабах. Предсказанный выигрыш при этом завышен (1.5B, τ=6: предсказано −19%, измерено −13%), поэтому отчёт правила служит ориентиром. Цена измерения — один проход на матрицу на битности пресета и один на реализуемый шаг вниз. Проход возобновляется с чистого остаточного потока на входе слоя матрицы: слои до неё квантование не меняет, результат побитно тот же, что у полного прохода (проверено гейтом), а работы в среднем вдвое меньше. Прибор — fp32-модель с весами, хранящимися в bf16: апкаст точен, поэтому это та же fp32-модель при вдвое меньшей памяти; эталон считается тем же прибором в том же процессе, и пол KL равен нулю по построению. 6.7 Что правило не делает Правило не спасает от отсутствия AW-статистики (раздел 4.3: без неё 2.9B разрушается) и не отменяет фиксированных правок пресета: o_proj слоя 0 и эмбеддинг в 6 бит остаются его стартовой точкой. На больших моделях измерение считается там, где хватает памяти, и переносится как файл со строгой подписью чекпоинта — так же, как imatrix для GGUF. 7. GPTQ на сетке пресета COMPRESSION Замечание. Таблицы разделов 7.2–7.5 и приложения B получены серверным прототипом GPTQ. Итоговые числа по файлам, которые пишет библиотека, приведены в разделе 9; точечные значения прототипа и библиотеки различаются в пределах разброса реализаций (раздел 7.6). Все измерения этого раздела — на чистых наборах (раздел 3.4). 7.1 Метод Я переношу GPTQ (Frantar et al., 2022) на асимметричную суперблочную сетку asym_sb6_aw: масштаб и сдвиг суперблока (256 колонок) вычисляются в его начале, по уже поправленным весам, тем же AW-поиском, что используется при обычной записи (RTN); колонки квантуются по одной; ошибка каждой колонки разносится на ещё не квантованные через H⁻¹. Проход последовательный: вход каждой матрицы снимается с модели, в которой предыдущие матрицы уже квантованы (порядок: r, k, v → o → ffn.key → ffn.value, голова в конце). Эмбеддинг — таблица поиска, GPTQ его не затрагивает. Формат файла, ядра и размер не меняются. Проверки корректности: при диагональной H коды побитно равны RTN (проверка 0); файл, записанный библиотекой, побитно равен fake-пути GPTQ (гейт). 7.2 Регуляризация важнее размера калибровки Первая версия: 48 окон корпуса пакета, демпфирование damp = 0.01. KL к RTN, чистый набор: модель текст en код 2.9B −21.1% −12.2 −37.9% 7.2B +0.4% +13.6 −21.4% 13.3B +4.7% +17.7 −21.4% На больших моделях английский текст стал хуже RTN. Причина — переобучение: 24.5 тыс. токенов на оценку H размером до 16384×16384. Это подтверждается двумя контрольными прогонами на 7.2B: damp 0.1 на тех же 48 окнах даёт −21.0% (en −17.1), а 82 окна при damp 0.01 дают лишь −4.9% (en +6.7). Дело именно в регуляризации, а не в числе окон. 7.3 Калибровочный корпус v2 600 окон по 512 токенов (307 тыс. токенов), собраны из открытых источников: доля состав en 30% FineWeb-Edu ru 20% FineWeb-2, русская литература, oasst2 код 20% UltraData-Code, 6 языков, плюс открытый код проекта zh 10% FineWeb-2, novel_text sr 10% кириллица и латиница пополам рассуждения 10% дистилляции двух моделей Порядок окон — взвешенный круг между группами, поэтому любой префикс сохраняет доли. Окна, содержащие 16-граммы любого оценочного набора, выброшены. 7.4 Результаты: GPTQ против RTN KL к RTN-пресету при damp 0.1 на чистых наборах (текст / en / ru / sr / код) и изменение Δppl: модель окон KL Δppl текст Δppl код 0.1B 150 −33.6 / −34.7 / −30.6 / −35.8 / −35.8 +7.80 → +5.12% — 1.5B 150 −39.6 / −41.3 / −39.8 / −37.7 / −46.7 +3.91 → +2.63% +3.84 → +2.18% 1.5B 600 −42.2 / −40.2 / −42.5 / −43.9 / −49.9 +3.91 → +2.55% +3.84 → +1.85% 2.9B 150 −38.7 / −37.5 / −38.4 / −40.1 / −43.1 +2.78 → +1.69% +2.33 → +1.53% 2.9B 600 −42.5 / −42.2 / −42.6 / −42.7 / −46.4 +2.78 → +1.49% +2.33 → +1.04% 7.2B 150 −22.8 / −18.6 / −22.4 / −27.5 / −34.2 +1.85 → +1.44% +1.22 → +0.84% 7.2B 600 −28.8 / −25.4 / −27.2 / −33.8 / −38.5 +1.85 → +1.31% +1.22 → +0.76% 13.3B 150 −21.0 / −17.3 / −19.9 / −26.4 / −37.3 +1.14 → +1.13% +1.11 → +0.45% 13.3B 600 −28.8 / −26.4 / −27.8 / −32.6 / −38.5 +1.14 → +0.86% +1.11 → +0.69% Ни один язык ни на одной модели не хуже RTN. damp 0.1 оказался лучше 0.01 и на малых моделях: на 1.5B и 2.9B при 150 окнах разница составляет 5–7 п. Большим моделям нужно больше окон: переход 150 → 600 даёт +6–8 п. по тексту на 7.2B и 13.3B и +3–4 п. на 1.5B и 2.9B. Английский выигрывает меньше других языков на всех масштабах. Парный бутстрэп по окнам (600 окон калибровки; приложение B.2) показывает, что GPTQ значимо лучше RTN по KL и по ppl на каждой модели, по тексту и по коду; хуже RTN по KL оказалось одно окно из 360 (7.2B, текст). 7.5 Вместе с автоподбором Измеряется KL к пресету без обоих методов, 600 окон калибровки. «Произведение» — ожидание при независимости методов: (1 − выигрыш GPTQ) × (1 − выигрыш автоподбора). модель KL текст произведение KL код Δppl текст Δppl код 0.1B −62.3% −59.8% −63.1% +7.80 → +2.84% +6.83 → +2.68% 0.4B −45.6% −47.9% −42.8% +4.43 → +2.72% +4.74 → +2.22% 1.5B −52.0% −52.3% −55.1% +3.91 → +2.11% +3.84 → +1.49% 2.9B −45.2% −53.4% −51.4% +2.78 → +1.28% +2.33 → +1.04% 7.2B −42.5% −43.5% −44.9% +1.85 → +1.07% +1.22 → +0.46% 13.3B −41.3% −43.9% −47.6% +1.14 → +0.58% +1.10 → +0.27% На пяти моделях из шести методы складываются почти мультипликативно, в пределах 3 п. Исключение — 2.9B: поверх GPTQ автоподбор добавляет лишь −4.7% по тексту (−9.2% по коду) вместо ожидаемых около −19%. Первая гипотеза (автоподбор на 2.9B тратит байты на o_proj слоя 0, а GPTQ уже чинит эту матрицу) оказалась неверной: o_proj слоя 0 хранится в bf16 в обоих пресетах и автоподбор её не трогает. Объяснение одними понижениями бит тоже неубедительно: доля понижений на 2.9B наибольшая, но на 13.3B лишь немного меньше, а сложение там почти мультипликативно. Затем выяснилось, что аномалия — артефакт пары прогонов прототипа (раздел 7.6): по файлам библиотеки автоподбор поверх GPTQ на 2.9B даёт −11.5% [−14.7; −8.2] и значим. Важная деталь: измерение автоподбора делается при RTN. Прямая проверка зависимости — снять измерение по модели, в которой остальные матрицы уже прошли GPTQ, и сравнить выбор; она не проведена (раздел 11). 7.6 Воспроизводимость и разброс GPTQ Детерминизм. Библиотечный GPTQ детерминирован: два прогона на разных картах и файл, собранный quantize() под нагрузкой сервера, побитно совпали (0.4B и 1.5B, REDUCTION, 144 тензора из 144). Разброс между реализациями. Две численно эквивалентные реализации дают разные коды. Прототип брал верхний множитель Холецкого, библиотека — нижний с транспонированием; разница порядка 1e-15, но GPTQ разносит её по всей матрице. Итог файла библиотеки против прототипа, тем же прибором: ΔKL, текст / код Δppl код 1.5B COMPRESSION +1.1 / +4.2% +1.83 против +1.49% 2.9B COMPRESSION −5.3 / +0.6% — 0.4B COMPRESSION +0.2 / +0.1% — Знак случайный. Значит, у каждого одиночного GPTQ-числа есть погрешность «реализации» порядка ±5% KL и до ±0.35 п. ppl. Сравнения внутри этого разброса не доказаны. Реплики. Чтобы измерить разброс, а не оценивать его по трём парам, библиотечный GPTQ повторён с переставленным порядком 600 окон калибровки: окна те же, меняется только порядок суммирования H, то есть возмущение того же рода, что смена множителя Холецкого. Файлы пишет quantize(), оценка — тот же прибор по записанному файлу; код здесь — открытый набор (48 окон). Умолчание COMPRESSION: 0.4B — 6 реплик, 1.5B и 2.9B — по 4; «только GPTQ» на 2.9B — 4 реплики. модель, плечо KL текст: sd / размах KL код: sd / размах размах Δppl текст / код, п. 0.4B, умолчание 0.6% / 1.6% 0.8% / 2.2% 0.38 / 0.37 1.5B, умолчание 1.3% / 2.8% 1.4% / 2.9% 0.19 / 0.15 2.9B, умолчание 1.2% / 2.7% 2.1% / 4.7% 0.27 / 0.19 2.9B, только GPTQ 0.6% / 1.5% 1.3% / 2.9% 0.28 / 0.13 Что из этого следует. Погрешность одиночного GPTQ-числа внутри библиотеки — около ±1.5% KL (одно sd), размах до 3–5%. Это меньше прежней оценки ±5%, сравнивавшей библиотеку с прототипом. Исходный порядок окон из облака реплик не выделяется (отклонение от среднего по перестановкам в пределах ±2%). По ppl разброс реплик сравним с интервалом по окнам: размах 0.15–0.4 п. при Δppl +1…+2.7%. Различия Δppl между плечами меньше 0.3 п. по одному файлу не доказаны. Шум отдельного окна — 4–8% KL (медиана sd по репликам). Поэтому счёт «в скольких окнах хуже» при эффектах порядка 10% ненадёжен, а среднее по окнам устойчиво. Автоподбор поверх GPTQ на 2.9B значим. Среднее по репликам, интервал — совместный бутстрэп по окнам и репликам: текст −11.5% [−14.7; −8.2], код −7.4% [−10.6; −3.8]; все 16 пар реплик дают выигрыш (текст от −12.8 до −9.2%). На 0.4B и 1.5B (одна реплика «только GPTQ») выигрыш по тексту −12…−13% и −12…−14%. Методы складываются чуть хуже произведения, на 2.9B сильнее всего: отношение измеренного к произведению — 2.9B текст 1.09 [1.05; 1.13], код 1.11 [1.07; 1.16]; 0.4B 1.03–1.05; 1.5B 1.04–1.07 по тексту и около 1.0 по коду. Прежние 1.18 на 2.9B сложились из двух отклонений прототипа в разные стороны: «только GPTQ» у прототипа 0.01560 против 0.01601 у библиотеки (среднее реплик), умолчание — 0.01486 против 0.01417. 7.7 Стоимость Время GPTQ на RTX 4090 при 600 окнах: модель карт время 1.5B 1 42 мин 2.9B 1 81 мин 7.2B 2 2.6 ч 13.3B 4 4.7 ч Активации калибровки держатся на хосте (2 × окна × 511 × D × 4 байта), слои разнесены по картам. На MacBook Air M4 16 ГБ без вентилятора, пресет REDUCTION, вызов quantize() целиком: модель время пик памяти своп (изменение за прогон) 0.1B 17 мин 7.7 ГБ +0 0.4B 49 мин 8.9 ГБ +0 1.5B 2 ч 26 мин 19.5 ГБ −1.4 ГБ На 1.5B пик превышает объём ОЗУ: macOS сжимает память, время на слой остаётся ровным (около 6 мин). Оценка «модель + активации», по которой библиотека решает, запускать ли GPTQ неявно, занижает пик в 2.4–3.4 раза. Файлы, собранные на Mac (цикл GPTQ на CPU), по реальному пути совпадают с серверными в пределах разброса реализаций (1.5B, Δppl по двум наборам: +0.20 / +0.06% против +0.27 / +0.04%). 8. GPTQ в пресете REDUCTION REDUCTION хранит проекции, cmix и голову в симметричной раскладке типа Q6_K (раздел 3.2). GPTQ переносится на эту сетку так же, как на асимметричную: сетка суперблока строится в его начале по поправленным весам, дальше идёт тот же цикл. Проверка 0 при диагональной H — побитное равенство RTN на 6 и 8 битах. Калибровка — те же 600 окон, damp 0.1. KL к RTN-REDUCTION на чистых наборах (текст / en / ru / sr / код) и Δppl. Время — на одной RTX 4090 при совместной загрузке карт другими прогонами, поэтому ориентировочное: модель KL Δppl текст Δppl код время 0.1B −22.4 / −28.7 / −19.5 / −20.6 / −27.9 +0.50 → +0.47% +0.60 → +0.48% 14 мин 0.4B −22.7 / −21.5 / −22.7 / −23.7 / −27.2 +0.29 → +0.21% +0.23 → +0.18% 29 мин 1.5B −28.4 / −24.5 / −29.1 / −31.4 / −32.1 +0.35 → +0.27% +0.07 → +0.05% 43 мин 2.9B −29.6 / −26.2 / −31.2 / −31.4 / −33.6 +0.15 → +0.09% +0.15 → +0.15% 75 мин 7.2B −21.1 / −12.6 / −23.6 / −27.3 / −24.3 +0.12 → +0.13% +0.09 → +0.09% 2.4 ч, 2 карты 13.3B −17.1 / −16.2 / −20.0 / −15.2 / −31.6 +0.07 → +0.07% +0.10 → +0.09% 5.2 ч, 4 карты* - Время на 4 картах при совместной загрузке сервера другими прогонами. Ни один язык ни на одной модели не хуже RTN по KL. Относительный выигрыш (−17…−30% по тексту) меньше, чем в COMPRESSION (−34…−42%): для 8 бит это ожидаемо, ошибка округления мала и компенсировать нечего. С размером модели выигрыш не растёт: на 7.2B и 13.3B он наименьший по тексту, а английский на 7.2B выигрывает всего −12.6%. По коду выигрыш держится на всех масштабах (−24…−34%). Парный бутстрэп по окнам (приложение B.4) подтверждает, что выигрыш по KL значим везде, на каждом языке каждой модели; самая слабая граница — английский на 7.2B, верхний край интервала −4.8%. По окнам GPTQ хуже RTN по KL не более чем в одном окне из 36 (текст) или 24 (код). Изменение ppl не различимо ни на одной модели: все интервалы накрывают ноль, их ширина ±0.1–0.15%, что сравнимо с самой деградацией REDUCTION (+0.1–0.5%). По знаку ppl чаще лучше: у четырёх моделей из шести точечная оценка по тексту отрицательна, у 7.2B и 13.3B — +0.01%. Два языка, где ppl наклоняется в худшую сторону: английский на 7.2B: +0.10% [−0.04; +0.24], P(хуже) = 0.92 — не значимо; русский на 13.3B: +0.11% [+0.03; +0.19], P(хуже) = 0.997 — значимо; KL этого же языка при этом −20.0% [−24.8; −15.6]. Это около 30 парных сравнений (6 моделей × 5 срезов) на уровне 95%. Одно ложное срабатывание в такой серии ожидаемо, поэтому единичный значимый случай не опровергает вывод, но он записан. Решение: GPTQ включён по умолчанию и в REDUCTION. Довод: REDUCTION служит базой для QLoRA и векторных моделей (эмбеддеров), где важна верность скрытых состояний исходной модели, а не ppl. Сетка в библиотеке та же, упаковка общая с RTN; RTN-файл после рефакторинга побайтно прежний. Библиотечный GPTQ на 0.1B воспроизводит прототип точно: KL и CE совпадают до шестого знака. Вывод. GPTQ в REDUCTION значимо приближает модель к bf16 по распределению (KL), но на этом наборе не даёт измеримого выигрыша по ppl. Для near-lossless пресета это выигрыш в верности исходной модели, а не в качестве на задаче; чтобы увидеть его по ppl, нужно на порядок больше окон. Байты при этом не меняются. 9. Итоговые результаты по шести размерам Здесь шесть масштабов измерены так, как результат получит пользователь: файлы пишет quantize() библиотеки, оценка идёт по записанному файлу одним прибором на чистых наборах (36 окон текста и 48 окон открытого кода). Интервалы Δppl — парный бутстрэп по окнам, 95%. Файл GPTQ имеет тот же размер, что RTN-файл той же конфигурации. Автоподбор — бюджет +0.5% файла; GPTQ — 600 окон калибровки v2, damp 0.1. Жирным выделено умолчание библиотеки (quantize(preset="compression") включает автоподбор и GPTQ). Размеры файлов (МБ и доля от bf16): модель bf16 REDUCTION COMPRESSION + автоподбор 0.1B 382 191 (50.1%) 134 (35.2%) 0.4B 902 434 (48.1%) 299 (33.2%) 1.5B 3055 1439 (47.1%) 997 (32.6%) 2.9B 5896 2743 (46.5%) 1892 (32.1%) 7.2B 14400 6648 (46.2%) 4576 (31.8%) 13.3B 26541 12178 (45.9%) 8377 (31.6%) Качество: модель плечо МБ KL текст KL код Δppl текст, % Δppl код, % 0.1B REDUCTION 191.3 0.00550 0.00555 +0.49 [+0.33; +0.65] +0.75 [+0.60; +0.90] 0.1B REDUCTION + GPTQ (умолчание) 191.3 0.00426 0.00399 +0.46 [+0.33; +0.59] +0.55 [+0.41; +0.69] 0.1B COMPRESSION 133.9 0.07420 0.06944 +7.76 [+6.81; +8.73] +6.61 [+5.84; +7.43] 0.1B - автоподбор 134.4 0.04524 0.04011 +5.13 [+4.59; +5.66] +3.56 [+3.13; +3.98] 0.1B - GPTQ 133.9 0.04838 0.04082 +4.95 [+4.33; +5.58] +3.60 [+3.11; +4.09] 0.1B - оба (умолчание) 134.4 0.02864 0.02476 +2.76 [+2.28; +3.25] +2.47 [+2.18; +2.77] 0.4B REDUCTION 434.1 0.00313 0.00280 +0.28 [+0.16; +0.40] +0.21 [+0.11; +0.32] 0.4B REDUCTION + GPTQ (умолчание) 434.1 0.00242 0.00205 +0.20 [+0.10; +0.31] +0.19 [+0.11; +0.28] 0.4B COMPRESSION 301.2 0.04805 0.04264 +4.40 [+3.75; +5.07] +4.38 [+3.85; +4.90] 0.4B - автоподбор 299.2 0.04038 0.03757 +4.19 [+3.67; +4.72] +3.56 [+3.07; +4.07] 0.4B - GPTQ 301.2 0.02956 0.02564 +2.96 [+2.44; +3.48] +2.45 [+2.02; +2.91] 0.4B - оба (умолчание) 299.2 0.02599 0.02306 +2.57 [+2.11; +3.04] +2.00 [+1.60; +2.42] 1.5B REDUCTION 1439.0 0.00239 0.00244 +0.35 [+0.22; +0.47] +0.13 [+0.02; +0.23] 1.5B REDUCTION + GPTQ (умолчание) 1439.0 0.00171 0.00162 +0.27 [+0.17; +0.37] +0.03 [-0.04; +0.10] 1.5B COMPRESSION 993.4 0.03779 0.04061 +3.90 [+3.40; +4.43] +3.85 [+3.40; +4.30] 1.5B - автоподбор 996.5 0.03119 0.03597 +3.29 [+2.86; +3.72] +3.59 [+3.10; +4.08] 1.5B - GPTQ 993.4 0.02135 0.02073 +2.54 [+2.13; +2.94] +1.76 [+1.44; +2.09] 1.5B - оба (умолчание) 996.5 0.01831 0.01874 +1.98 [+1.55; +2.44] +1.69 [+1.28; +2.13] 2.9B REDUCTION 2743.3 0.00177 0.00180 +0.15 [+0.05; +0.26] +0.05 [-0.04; +0.14] 2.9B REDUCTION + GPTQ (умолчание) 2743.3 0.00125 0.00120 +0.09 [-0.01; +0.19] +0.05 [-0.02; +0.12] 2.9B COMPRESSION 1885.5 0.02708 0.02976 +2.77 [+2.33; +3.22] +2.63 [+2.21; +3.03] 2.9B - автоподбор 1891.8 0.02192 0.02480 +2.12 [+1.65; +2.58] +2.18 [+1.77; +2.59] 2.9B - GPTQ 1885.5 0.01610 0.01514 +1.75 [+1.42; +2.08] +1.30 [+1.02; +1.58] 2.9B - оба (умолчание) 1891.8 0.01406 0.01394 +1.32 [+1.09; +1.56] +1.05 [+0.76; +1.34] 7.2B REDUCTION 6647.8 0.00119 0.00112 +0.12 [+0.04; +0.21] +0.11 [+0.04; +0.18] 7.2B REDUCTION + GPTQ (умолчание) 6647.8 0.00093 0.00081 +0.13 [+0.07; +0.20] +0.08 [+0.02; +0.14] 7.2B COMPRESSION 4555.0 0.01682 0.01686 +1.88 [+1.56; +2.20] +1.51 [+1.23; +1.79] 7.2B - автоподбор 4576.0 0.01331 0.01437 +1.52 [+1.17; +1.87] +1.09 [+0.80; +1.40] 7.2B - GPTQ 4555.0 0.01201 0.00978 +1.02 [+0.68; +1.37] +0.71 [+0.49; +0.94] 7.2B - оба (умолчание) 4576.0 0.00992 0.00857 +0.95 [+0.68; +1.22] +0.67 [+0.48; +0.86] 13.3B REDUCTION 12178.4 0.00082 0.00090 +0.07 [+0.00; +0.13] +0.11 [+0.05; +0.16] 13.3B REDUCTION + GPTQ (умолчание) 12178.4 0.00068 0.00065 +0.07 [+0.02; +0.13] +0.10 [+0.05; +0.15] 13.3B COMPRESSION 8337.1 0.01128 0.01228 +1.15 [+0.87; +1.44] +1.12 [+0.85; +1.39] 13.3B - автоподбор 8377.0 0.00889 0.01020 +0.88 [+0.64; +1.12] +0.75 [+0.53; +0.96] 13.3B - GPTQ 8337.1 0.00814 0.00705 +0.83 [+0.59; +1.08] +0.38 [+0.21; +0.56] 13.3B - оба (умолчание) 8377.0 0.00687 0.00615 +0.70 [+0.49; +0.93] +0.40 [+0.23; +0.57] Что видно. Относительная деградация монотонно падает с размером модели в обоих пресетах. COMPRESSION, KL текста: 0.074 → 0.048 → 0.038 → 0.027 → 0.017 → 0.011. REDUCTION: 0.0055 → 0.0031 → 0.0024 → 0.0018 → 0.0012 → 0.0008. Аномалия 2.9B из раздела 4 после правок пресета (o_proj слоя 0, эмбеддинг в 6 бит) в этой линейке уже не видна. Потери умолчания COMPRESSION падают с размером и по Δppl: +2.76, +2.57, +1.98, +1.32, +0.95, +0.70% по тексту. Сжатие растёт с размером. Доля «неконтролируемых» тензоров (LoRA-ветки, малые тензоры, нормы) убывает: REDUCTION 50% → 46%, COMPRESSION 35% → 31.4%. Оба метода окупаются сильнее всего на самой малой модели. На 0.1B умолчание снимает 62% KL пресета, на остальных 41–52% (раздел 7.5). Поэтому умолчание COMPRESSION на 0.4B (+2.57%) лучше голого пресета на 1.5B (+3.90%), а умолчание на 2.9B (+1.32%) — голого пресета на 7.2B (+1.88%). Автоподбор почти не стоит байт. Прибавка составляет 0.1–0.2% файла; на 0.4B размер даже уменьшился. Бюджет +0.5% не выбирается целиком: правило упирается в τ. О строгости монотонности. Для голых пресетов монотонность по размеру держится с запасом: интервалы KL соседних масштабов не пересекаются ни в REDUCTION, ни в COMPRESSION (по таблице прототипа). Для умолчания COMPRESSION она держится по точечным оценкам, но интервалы соседних масштабов пересекаются (например, KL текста 0.0279 [0.0263; 0.0296] у 0.1B против 0.0260 [0.0242; 0.0278] у 0.4B — по предварительной таблице прототипа — и аналогично у пары 1.5B/2.9B). Интервалы Δppl соседних масштабов в основном перекрываются; строго различимы далёкие масштабы, например 0.1B (+2.76 [+2.28; +3.25]) и 13.3B (+0.70 [+0.49; +0.93]). Замена закрытого набора кода на открытый ни одного вывода не сдвинула: KL по двум наборам кода расходится в пределах 10%. Рис. 6. Потери ppl по шести размерам модели Рис. 6. Δppl относительно bf16 для шести размеров: RTN, автоподбор, GPTQ и их сочетание. Данные из таблицы итогов этого раздела. 10. Сравнение с GGUF и MLX при равном размере файла Чужие форматы собраны из того же чекпоинта g1j и измерены тем же прибором, что мои файлы: их квантованные веса деквантуются и подставляются в эталонную модель, дальше — те же окна и та же KL к bf16. GGUF: llama-quantize, Q4_K_M и Q6_K с imatrix по тем же 600 окнам, что у моего GPTQ; деквантование — gguf-py. В этой сборке Q4_K_M держит голову в Q6_K, а LoRA — в F16. MLX: схема опубликованных файлов (affine, группа 64). int6 — всё в 6 бит; int4 — проекции и FFN в 4 бита, эмбеддинг, голова и LoRA в 6. Коды снимает сам mx.quantize, деквантование на сервере побитно равно mx.dequantize. Мои файлы равного размера: сетка REDUCTION (симметричная, тип Q6_K) с 6 битами. sym6 — проекции, эмбеддинг и голова в 6 бит (размер int6); sym6h8 — только проекции в 6 бит, эмбеддинг и голова в 8 (размер Q6_K). Автоподбора здесь нет: это не оптимизированные точки, а простейшая замена битности. 1.5B формат МБ KL текст KL код Δppl текст, % Δppl код, % MLX int4 970.4 0.11642 0.12479 +12.71 [+11.43; +14.03] +12.14 [+11.14; +13.16] GGUF Q4_K_M (imatrix) 990.1 0.03926 0.04049 +4.07 [+3.48; +4.72] +3.76 [+3.32; +4.21] мой COMPRESSION (умолчание) 996.5 0.01831 0.01874 +1.98 [+1.54; +2.44] +1.69 [+1.28; +2.12] мой sym6, без GPTQ 1272.3 0.00375 0.00402 +0.51 [+0.35; +0.67] +0.29 [+0.15; +0.43] мой sym6 + GPTQ 1272.3 0.00260 0.00251 +0.31 [+0.17; +0.45] +0.16 [+0.04; +0.27] MLX int6 1272.4 0.00901 0.00971 +0.83 [+0.64; +1.03] +0.54 [+0.34; +0.75] GGUF Q6_K (imatrix) 1336.1 0.00406 0.00426 +0.48 [+0.34; +0.61] +0.27 [+0.15; +0.39] мой sym6h8, без GPTQ 1339.4 0.00304 0.00329 +0.41 [+0.27; +0.54] +0.21 [+0.07; +0.33] мой sym6h8 + GPTQ 1339.4 0.00208 0.00207 +0.27 [+0.16; +0.38] +0.11 [+0.01; +0.22] мой REDUCTION (умолчание) 1439.0 0.00171 0.00162 +0.27 [+0.17; +0.37] +0.03 [-0.04; +0.10] 2.9B формат МБ KL текст KL код Δppl текст, % Δppl код, % MLX int4 1824.2 0.50748 0.46978 +66.14 [+53.11; +81.85] +55.45 [+47.30; +64.44] GGUF Q4_K_M (imatrix) 1875.8 1.37847 1.50555 +304.99 [+203.92; +497.81] +328.67 [+201.28; +647.98] мой COMPRESSION (умолчание) 1891.8 0.01406 0.01394 +1.32 [+1.09; +1.56] +1.05 [+0.76; +1.34] мой sym6, без GPTQ 2451.4 0.00288 0.00305 +0.27 [+0.12; +0.42] +0.16 [+0.05; +0.26] мой sym6 + GPTQ 2451.4 0.00205 0.00206 +0.22 [+0.10; +0.34] +0.05 [-0.04; +0.15] MLX int6 2453.4 0.01514 0.01967 +1.16 [+0.75; +1.54] +1.67 [+1.33; +2.02] мой sym6h8, без GPTQ 2535.3 0.00223 0.00239 +0.22 [+0.11; +0.34] +0.15 [+0.05; +0.25] мой sym6h8 + GPTQ 2535.3 0.00155 0.00153 +0.20 [+0.10; +0.31] -0.01 [-0.11; +0.08] GGUF Q6_K (imatrix) 2567.9 0.00512 0.00714 +0.33 [+0.11; +0.53] +0.63 [+0.42; +0.85] мой REDUCTION (умолчание) 2743.3 0.00125 0.00120 +0.09 [-0.01; +0.19] +0.05 [-0.02; +0.12] Отношения KL «чужой формат / мой файл» (парный бутстрэп по окнам; значение > 1 означает, что мой файл точнее). В скобках — разница размера моего файла относительно чужого. 1.5B чужой / мой размер моего KL текст KL код GGUF Q4_K_M (imatrix) / мой COMPRESSION (умолчание) +0.7% 2.14 [1.98; 2.30] 2.16 [2.05; 2.28] MLX int4 / мой COMPRESSION (умолчание) +2.7% 6.36 [5.84; 6.92] 6.66 [6.20; 7.14] MLX int6 / мой sym6 + GPTQ -0.0% 3.47 [3.23; 3.74] 3.86 [3.72; 4.01] MLX int6 / мой sym6, без GPTQ -0.0% 2.40 [2.29; 2.53] 2.42 [2.35; 2.49] GGUF Q6_K (imatrix) / мой sym6h8 + GPTQ +0.2% 1.95 [1.82; 2.09] 2.06 [1.95; 2.17] GGUF Q6_K (imatrix) / мой sym6h8, без GPTQ +0.2% 1.34 [1.27; 1.40] 1.29 [1.24; 1.35] MLX int6 / мой COMPRESSION (умолчание) -21.7% 0.49 [0.45; 0.53] 0.52 [0.48; 0.56] GGUF Q6_K (imatrix) / мой REDUCTION (умолчание) +7.7% 2.37 [2.18; 2.58] 2.63 [2.49; 2.76] MLX int6 / мой REDUCTION (умолчание) +13.1% 5.27 [4.88; 5.71] 5.98 [5.70; 6.28] 2.9B чужой / мой размер моего KL текст KL код GGUF Q4_K_M (imatrix) / мой COMPRESSION (умолчание) +0.9% 98.06 [75.72; 129.80] 108.00 [81.83; 149.56] MLX int4 / мой COMPRESSION (умолчание) +3.7% 36.10 [29.93; 42.99] 33.70 [29.72; 38.19] MLX int6 / мой sym6 + GPTQ -0.1% 7.38 [6.57; 8.42] 9.57 [8.71; 10.57] MLX int6 / мой sym6, без GPTQ -0.1% 5.26 [4.72; 5.95] 6.46 [5.93; 7.05] GGUF Q6_K (imatrix) / мой sym6h8 + GPTQ -1.3% 3.31 [2.92; 3.76] 4.65 [4.25; 5.09] GGUF Q6_K (imatrix) / мой sym6h8, без GPTQ -1.3% 2.30 [2.08; 2.56] 2.99 [2.75; 3.25] MLX int6 / мой COMPRESSION (умолчание) -22.9% 1.08 [0.94; 1.26] 1.41 [1.27; 1.57] GGUF Q6_K (imatrix) / мой REDUCTION (умолчание) +6.8% 4.10 [3.63; 4.69] 5.97 [5.45; 6.51] MLX int6 / мой REDUCTION (умолчание) +11.8% 12.13 [10.62; 14.15] 16.45 [14.99; 18.18] Что из этого следует. При равном размере
MacBook Air(M5)でローカルLLM環境を構築してみた はじめに MacBook Air(M5、メモリ24GB)にローカルLLM環境を構築して「とりあえず動かしてみる」という記事です。 結論として、今回はgemma4を使ったのですが、軽量なモデルでも思ったより回答までに時間がかかるなという印象です。 (普段ClaudeCodeを使っているので、期待値が高すぎたのかも。) とはいえ、Claudeがリミットになったので一時的にローカルLLMで代用したいときはありなのかなと思います。 機密情報を扱うのでオフライン環境でLLMを使いたいなどの明確な目的・理由がある場合には、素直にパソコンのスペックを上げましょう。 ! 本記事はあくまでも「使ってみた」的な実験記事です。ローカルLLMの利用目的があるわけでもなく、どれくらい動くのか試してみたい、くらいの記事です。主観・体感をまとめたものとして読んでください。気になる人は自分でやってみてください。意外と簡単に環境構築できます。 PCスペック 私のMacbook Airは以下の通りです。 4月頃にIntel Macから買い替えたのですが、なんで24GBにしたのかは覚えていません。。 項目 スペック 機種 MacBook Air チップ Apple M5 メモリ 24GB(ユニファイドメモリ) OS macOS 27.0(Golden Gate) ローカルLLM実行環境 ローカルLLM環境にはいくつか種類がありますが、今回は手軽さを優先して以下の2つにしています。 opencode:ターミナルで動くオープンソースのAIコーディングエージェント。ClaudeやCodexの他にローカルで動作するモデルも使える。 Ollama:ローカルでLLMを動かすためのツール。CLIでモデルのダウンロード(ollama pull)から起動まで完結し、127.0.0.1 でAPIサーバーとしても動作する。今回はopencodeからこのAPI利用する。(Ollama単体でもターミナルからチャットできる。) VS Code:コードエディタ。opencodeの拡張機能を導入して、VScode上でopencodeを使う。 Ollama構築 CLIとAPIサーバーだけで足りるので、Homebrewでインストールします。 公式サイトからダウンロードでも大丈夫です。 brew install ollama インストール後、Ollamaを起動。ターミナルを閉じるとOllamaも終了します。 ollama serve Ollamaを自動起動したい場合 ログイン時に自動実行したい場合は、以下でhomebrewで管理してくれます。 Ollama公式からGUIアプリをインストールすると、アプリでログイン時の動作も管理できるみたいです。(試してません。) brew services start ollama 状態確認と停止は以下の通りです。 brew services list # 状態確認 brew services stop ollama モデルをダウンロード ollamaで動かせるモデルはここからダウンロードできます。 自分の端末のスペックをAIに投げて、おすすめの(動作しそうな)モデルを聞いてみてください。 今回は、GoogleがGeminiと同じ技術をもとに開発した軽量モデル「gemma」を使います。 24GBだと量子化版(ざっくり言うと情報量を落とした軽量版らしい)が現実的みたい。 なので、以下をとりあえずダウンロード(コマンドはpull)してみる。軽いモデルでも数GBはあるので、色々試した後はollama listで一覧を確認して、ollama rm <モデル名>で削除できます。 ollama pull gemma4:e2b ollama pull gemma4:e4b ollama pull gemma4:e2b-mlx ollama pull gemma4:e4b-mlx ollama pull gemma4:12b ollama show gemma4:e2bで表示されるquantizationの項目を見ると、量子化方式を確認できます。gemma4:e2bはQ4_K_M(4bit量子化)でした。 mlxとは タグ名の-mlxは、Apple製の機械学習フレームワークMLX向けに作られたモデルを指します。MLXはApple Siliconの、CPUとGPUで同じメモリを共有するユニファイドメモリを活かせるように設計されています。 ollama show gemma4:e2b-mlxで確認すると、量子化方式はnvfp4でした。gemma4:e2b(Q4_K_M)と量子化の形式が違うので、速さや回答の違いを比べてみると面白いかもしれません。 ちなみに、gemma3(gemma3n:e2b、gemma3n:e4b、gemma3:4b-it-qat)もダウンロードしましたが、opencodeからの呼び出しには対応していませんでした。 opencode構築 インストール opencodeもHomebrewでインストールできます。opencodeのデスクトップアプリもあるみたいですが今回はVScodeから使うので、brew install opencodeでインストールします。 brew install opencode opencode --version ターミナルから使う場合は、プロジェクトフォルダまで移動して、opencodeで実行できます。 cd ~/your/project/ opencode ollamaへの接続設定 opencodeの設定ファイル~/.config/opencode/opencode.jsonにollamaへの接続を定義します。 ollamaはOpenAI互換のAPI(127.0.0.1 modelsのキーには、ollama listに表示されるモデル名をそのまま書きます。以下はこの記事でpullしたモデルを定義した例です。opencodeで使いたいモデルを増やしたいときには、ollama pullしたあとに追記します。 ~/.config/opencode/opencode.json { "$schema": "opencode.ai", "provider": { "ollama": { "npm": "@ai-sdk/openai-compatible", "name": "Ollama", "options": { "baseURL": "127.0.0.1" }, "models": { "gemma4:12b": { "name": "gemma4:12b" }, "gemma4:e2b": { "name": "gemma4:e2b" }, "gemma4:e4b": { "name": "gemma4:e4b" }, "gemma4:e2b-mlx": { "name": "gemma4:e2b-mlx" }, "gemma4:e4b-mlx": { "name": "gemma4:e4b-mlx" } } } } } VSCode構築 VSCode側は環境構築というほどでもなく、拡張機能でopencodeを入れるだけ。エディタ上からopencodeを呼び出せるようになります。 設定は上記のopencode.jsonを共通で参照するので、拡張機能側で別途モデルを設定する必要はありません。 使った感想 このMacBook Airでは、Pythonのhelloを出力するだけでもClaude Codeなどの有料プランより遅く感じました。普段からClaudeを使っている人には、物足りなく感じると思います。 どんな用途に向いていそうか ネットに繋がらない・繋げたくない環境で、簡単なコード生成や質問応答を試したいとき クラウドAIが使えなくなったときの、応急的な作業継続 スペック次第でこの辺りの体感は変わってくるはずなので、気になる人はまず自分の環境で試してみてください。
Muse Spark Open: 816B parámetros abiertos de Meta Meta acaba de liberar los pesos de Muse Spark Open, un modelo de 816.000 millones de parámetros que corre completo en dos Mac Studio con chip M5 Ultra, sin pasar por ningún servidor en la nube. Es la versión de pesos abiertos del sistema que Meta Superintelligence Labs presentó semanas atrás, y llega con una ventana de contexto de 1 millón de tokens pensada para ingeniería de software de varios pasos y trabajo agéntico prolongado. TL;DR - Meta publicó los pesos de Muse Spark Open, modelo MoE de 816.000 millones de parámetros con 42.000 millones activos.- La ventana de contexto llega a 1 millón de tokens, pensada para tareas de ingeniería de software largas.- Dos Mac Studio con chip M5 Ultra y MLX corren el modelo completo gracias a 512 GB de memoria unificada por equipo.- Los kernels Helion en SGLang aceleran la inferencia entre 1,03 y 3,95 veces en GPU Nvidia y AMD.- El modelo está disponible en Hugging Face y vía APIs de Together AI, Fireworks, Baseten, Groq, Modal, Nebius y OpenRouter. Qué es Muse Spark Open Muse Spark Open es el modelo de pesos abiertos derivado de Muse Spark, el sistema multimodal de mezcla de expertos de Meta con 816.000 millones de parámetros totales, 42.000 millones activos por token y una ventana de contexto de 1 millón de tokens, pensado para programación y trabajo agéntico de varios pasos. El modelo es nativamente multimodal: procesa texto e imágenes dentro de la misma arquitectura de mezcla de expertos (MoE), y activa solo 42.000 millones de los 816.000 millones de parámetros en cada token. Esa activación parcial es lo que distingue a un MoE de un modelo denso, porque el costo de cómputo por token se parece al de un modelo mucho más chico, aunque la memoria necesaria para guardar todos los expertos sigue siendo la de un modelo de 816.000 millones. Qué pasó El modelo de pesos abiertos de Meta ya está disponible para descargar en Hugging Face, Amazon SageMaker AI, Google Cloud Model Garden y Microsoft Foundry Models. Para quien prefiera no alojar el modelo, siete plataformas lo sirven como API: Baseten, Fireworks AI, Groq, Modal, Nebius, OpenRouter y Together AI. Muse Spark Open corre sobre los motores de inferencia más usados del momento: SGLang, vLLM, TensorRT-LLM, Hugging Face Transformers, Apple MLX, llama.cpp y Unsloth, con optimizaciones específicas para silicio de AMD, Apple, Intel y Nvidia. Quien quiera afinar el modelo para un caso propio puede hacerlo con PyTorch, usando el framework de entrenamiento distribuido TorchTitan. El framework MLX, usado para correr el modelo en Apple Silicon, es de código abierto desde 2023. Contexto e historia Meta Superintelligence Labs presentó la versión cerrada de Muse Spark hace unas semanas como el primer modelo de ese laboratorio. La novedad de octubre no es el modelo en sí, sino que Meta decidió abrir sus pesos y lo presenta, según el propio anuncio, como el modelo de pesos abiertos más capaz hecho en Estados Unidos hasta la fecha. La jugada ubica al modelo de pesos abiertos de Meta en una carrera donde China venía marcando el ritmo: modelos abiertos como los de Moonshot AI, Z.ai o DeepSeek llevaban meses liderando los rankings de pesos abiertos en benchmarks como Artificial Analysis. Meta responde con un modelo grande, pero con un argumento distinto: que corre en hardware de escritorio, algo que ningún rival de ese tamaño ofrecía hasta ahora. Detalles técnicos: cómo reparten la memoria los dos Mac Studio El motivo técnico por el que dos Mac Studio alcanzan para correr un modelo de 816.000 millones de parámetros está en la arquitectura de memoria unificada de Apple. Cada chip M5 Ultra permite que la GPU integrada use hasta 512 GB de esa memoria, así que entre los dos equipos suman un terabyte disponible para guardar los pesos del modelo completo. El ancho de banda es la otra mitad de la ecuación. El M5 Ultra entrega 1,2 TB/s de ancho de banda de memoria, una cifra que alcanza porque un modelo MoE no necesita leer los 816.000 millones de parámetros en cada paso: con el router eligiendo qué expertos activar, cada token solo necesita leer la porción de 42.000 millones de parámetros que le toca. MLX se encarga de repartir esa porción entre las dos máquinas para que, de cara al modelo, se comporten como una sola con el doble de memoria. El M5 Ultra reparte hasta 512 GB de memoria unificada por chip entre CPU y GPU. flowchart TD A["Agente Muse Code"] --> B["Router MoE del modelo"] B --> C["Mac Studio 1: M5 Ultra, 512 GB"] B --> D["Mac Studio 2: M5 Ultra, 512 GB"] subgraph "Memoria unificada combinada: ~1 TB" C D end Sobre esa base, Meta corrió pruebas agénticas completas en el hardware local, sin tocar la nube: usando Muse Code, su asistente de programación, sobre el modelo de pesos abiertos, el equipo aplicó cambios reales a Ice Cubes, un cliente de Mastodon de código abierto para iOS, con correcciones de bugs, funciones nuevas y compilación completa. En otra prueba, el modelo manejó DaVinci Resolve de punta a punta, revisando metraje fotograma por fotograma para encontrar los mejores momentos y recortarlos en formato vertical. En el otro extremo del hardware, para quien corre el modelo en GPU de centro de datos, Meta publicó kernels GPU propios llamados Helion dentro de SGLang. Esos kernels autoajustables entregan entre 1,03 y 3,95 veces más velocidad a nivel de kernel en GPU de Nvidia y AMD, y un 3% de mejora de punta a punta en una AMD MI350. OpciónCuándo usarlaVentajaLimitaciónAPI en la nube (Together AI, Fireworks, Groq, etc.)Para integrar el modelo sin gestionar hardwareSin inversión inicial, escala solaDepende de la disponibilidad y precio del proveedorPesos propios en GPU de centro de datosPara volumen alto o afinado con TorchTitanControl total y los kernels Helion aceleran la inferenciaRequiere GPU Nvidia o AMD de gama altaDos Mac Studio M5 Ultra en localPara correr el modelo completo sin nube ni GPU de servidorPrivacidad total, sin costo recurrente de APIEl hardware ronda varios miles de dólares y hay que mantenerlo > 💡 Tip: antes de comprar el hardware, probá el modelo de pesos abiertos de Meta vía API (Together AI u OpenRouter, por ejemplo) para confirmar que el caso de uso lo justifica. Impacto y análisis Para un equipo de desarrollo, la diferencia práctica no es el tamaño del modelo sino dónde corre. Un modelo de 816.000 millones de parámetros servido por API no es noticia en 2026: lo distinto es que ahora existe la opción de tenerlo corriendo en una oficina, sin que el código fuente o el footage de un proyecto salgan de la red local. Eso importa para equipos con compromisos de confidencialidad o que simplemente no pueden mandar datos sensibles a un proveedor externo. La licencia también es parte del cálculo. Meta libera el modelo bajo la Muse Model License, con términos comerciales limitados que permiten a buena parte de los desarrolladores usarlo gratis, aunque no es una licencia open source en el sentido estricto: empresas por encima de cierto umbral de usuarios suelen quedar fuera de la gratuidad, el mismo patrón que Meta ya usó con la familia Llama. > 📌 Nota: pesos abiertos no es lo mismo que código abierto. La licencia de Meta restringe el uso comercial a partir de cierta escala, algo que conviene revisar antes de integrar el modelo de pesos abiertos de Meta en un producto. El otro ángulo es la seguridad. Meta dice haber evaluado el modelo bajo su Meta Superintelligence Scaling Framework y haberle aplicado entrenamiento de seguridad adicional por tratarse de pesos abiertos, ya que una vez publicados los pesos no hay forma de revocarlos ni de parchear el modelo remotamente. Qué sigue Meta prometió un reporte de evaluación completo próximamente, además de la ficha de modelo ya publicada en Hugging Face. La compañía invita a compartir trabajos construidos con el modelo en los foros de Hugging Face, el canal que suele usar para medir adopción real antes de iterar en la siguiente versión. Si la adopción crece, el próximo capítulo lógico es el mismo que ya atravesaron Llama y los modelos abiertos chinos: afinados de la comunidad para tareas específicas, y comparaciones de rendimiento frente a Kimi K3, GLM-5.2 o Qwen3.8 en benchmarks independientes. 📖 Resumen en Telegram: Ver resumen Si tenés dos Mac Studio M5 Ultra a mano, el camino más corto para probarlo es instalar el adaptador MLX y descargar los pesos directo desde la colección de Muse Spark Open en Hugging Face. Preguntas frecuentes ¿Qué es Muse Spark Open y quién lo publicó? Es el modelo de pesos abiertos que Meta Superintelligence Labs derivó de su sistema Muse Spark, con 816.000 millones de parámetros totales y 42.000 millones activos por token. ¿Qué hardware necesito para correr el modelo de pesos abiertos de Meta en casa? Según el anuncio de Meta, la configuración validada son dos Mac Studio con chip M5 Ultra, usando el framework MLX para repartir el modelo entre ambos equipos. ¿En qué se diferencia el modelo abierto de Meta de la versión cerrada de Muse Spark? Comparten arquitectura y tamaño, pero la versión abierta publica los pesos para descarga y autoalojamiento, mientras que la versión cerrada solo se usa a través de los productos de Meta. ¿Qué licencia tiene el MoE abierto de Meta? Se distribuye bajo la Muse Model License, con términos comerciales limitados que permiten uso gratuito a la mayoría de los desarrolladores. ¿Dónde se puede probar la IA abierta de Meta Superintelligence Labs? Vía API en Baseten, Fireworks AI, Groq, Modal, Nebius, OpenRouter y Together AI, o descargando los pesos desde Hugging Face, Amazon SageMaker AI, Google Cloud Model Garden o Microsoft Foundry Models. Referencias - Meta AI Research: anuncio oficial del modelo, con benchmarks y detalles de licencia.- Hugging Face: colección con los pesos descargables del modelo.- GitHub: ml-explore/mlx: repositorio oficial del framework MLX usado para correr el modelo en Apple Silicon.- PyTorch: framework usado para personalizar el modelo con TorchTitan. 📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.
I missed this addition of 1-bit affine quantization to MLX 👀 PR: mlx#3161 t.co
Grateful as @zcbenz merged my first contribution to #MLX a small M1 quantized-matmul dispatch tuning. A year ago I decided to learn @Apple Silicon from scratch. Getting reviewed and merged by the person who maintains MLX is humbling in the best way 🙌 mlx#4629
How to install and tune oMLX on M5 Max 128 GB --- title: How to install and tune oMLX on M5 Max 128 GB published: true description: Recipe for oMLX 0.6.4 on a Mac M5 Max with 128 GB: install, Claude Code and advanced settings with screenshots (Lightning MTP, VLM MTP and DFlash). tags: macos, llm, ai, apple canonical_url: jacar.es cover_image: media.jacar.es --- oMLX is an LLM inference server built on MLX, the framework Apple shipped in December 2023 for Apple Silicon. It adds continuous batching, two-tier KV cache (RAM + SSD), and an OpenAI- and Anthropic-compatible API. On a Mac M5 Max with 128 GB of unified memory you can hold three or four large models at once with TurboQuant 3.5-bit on KV cache. That is enough to feed chat, agent and IDE in parallel. This guide collects the configuration tested in May 2026 to get the most out of that combination. Quick answers Which oMLX version do I use and how do I install it? Version 0.6.4 (released 29 August 2026, Apache 2.0). The direct route: download the .dmg from GitHub Releases, open it and drag to Applications. Or install via brew tap jundot/omlx https://github.com/jundot/omlx and brew install jundot/omlx/omlx. The first load of the panel at http://localhost:8000/admin prompts for the API key. The full Homebrew route, including the service that starts on its own, is in installing, updating and uninstalling oMLX with Homebrew. What TurboQuant setting makes sense on 128 GB? 3.5-bit. vLLM’s independent analysis published 11 May 2026 shows 3.5-bit matches full-precision quality with about 4x less KV-cache memory. On M5 Max that turns 128k context from "blows past available RAM" into "fits alongside other loaded models." How much faster is it with the advanced 0.6.4 settings? Between 1.19x and 1.88x when generating code, and between 1.07x and 1.47x with 15,000 tokens of context, measured through the API on 15 September 2026. The biggest gain came from Lightning MTP on Qwen3.8-27B oQ4e, from 21.1 to 39.7 tok/s, and it needs a build of the model that keeps the MTP head. The details are in the advanced settings. Which models can I load concurrently on 128 GB? Primary: unsloth/Qwen3.6-35B-A3B-MLX-8bit (37.7 GB, MoE with 3B active). Fast helper: Qwen3-14B-Instruct-mlx-4bit (8 GB). Vision: Qwen2.5-VL-32B-mlx-4bit (18 GB). Embeddings: BGE-M3-mlx (1.2 GB). Reranker: ModernBERT-base-mlx (150 MB). Comfortable sum: ~65 GB with headroom. How do I point Claude Code at the local endpoint? Since 0.5.0 the short way is omlx launch claude, which exports the variables for you. By hand: export ANTHROPIC_BASE_URL=http://127.0.0.1:8000, ANTHROPIC_AUTH_TOKEN=<your_api_key> and the three ANTHROPIC_DEFAULT_*_MODEL variables (Opus/Sonnet/Haiku). Launch with claude --bare to drop the system prompt to ~1,795 tokens. The dashboard’s Claude Code with oMLX section assembles the full command for you. Does this replace Claude Opus 4.7? Not one for one. Claude Code is tuned for Claude’s tool-use format; a non-Claude model behind the endpoint loses reliability in agentic loops. Use this for offline work, sensitive data that should not leave the Mac, or as a fallback when api.anthropic.com is rate-limiting you. Install The easiest path: download the .dmg from GitHub Releases, open it and drag the app to Applications. The server starts in the background with a menu-bar icon. Or install via Homebrew: brew tap jundot/omlx https://github.com/jundot/omlx brew install jundot/omlx/omlx omlx start omlx serve --model-dir ~/models The first hit to http://localhost:8000/admin prompts for the API key. By default oMLX ships with no API key set (empty field), which is fine for local testing. If the instance only listens on 127.0.0.1 this is safe; the moment you expose it to the LAN or plan to share the endpoint, go to Settings → Auth & Info and set a long random string. The section accepts multiple keys at once. !Login screen of the oMLX admin panel at localhost:8000/admin, with the API Key field empty and a "Stay signed in for 30 days" checkbox Server settings for 128 GB Settings → Global Settings holds the full server configuration. The decisions that matter on a 128 GB machine: - Server → Host: Localhost only (127.0.0.1). If you open it to the LAN, put real auth in front first. - Server → Port: 8000 by default. - Resource Management → Memory Limit (Total): Auto. oMLX subtracts what macOS reserves; on 128 GB you end up around 110-114 GB for inference. - Resource Management → Memory Limit (Models Only): Auto. Keeps a percentage for activations, KV cache and auxiliary processes. - Resource Management → Hot Cache Limit: 10%. Intermediate RAM-tier KV cache. With TurboQuant enabled on the models that support it (see below), 10% is the value people running this setup in practice land on for 128 GB. With TurboQuant off and a single model loaded you can drop to Off, but you stop gaining. - Resource Management → Cold Cache Limit (SSD Cache): 10%. Around 80 GB of SSD for cold tokens without saturating it. - Resource Management → Max Concurrent Requests: 16. Comfortable for a single user with an agent, a chat session and an IDE pinging at once. Raise to 32 if you share with a small team. - Resource Management → Idle Timeout: None. Keeps models warm; the first token arrives in well under a second instead of seconds later. - Generation Defaults → Max Context Window: 256000 as the global default. The Qwen3.6 family holds up at long context, and with TurboQuant the effective RAM stretches enough to actually use it. Each model can be capped lower in Model Settings. - Generation Defaults → Max Tokens: 64000. Upper bound per response. Going higher only matters if you plan to generate full books in one shot. - Generation Defaults → Temperature: 1.0 for general use, 0.2-0.5 for code models. - Model Settings → Experimental Features → TurboQuant KV Cache: enable at 3.5-bit on the large dense models. vLLM’s independent study published 11 May 2026 of Google’s TurboQuant shows 3.5-bit matches full-precision quality with roughly 4x less KV-cache memory. On M5 Max that makes 128k context fit on models where FP16 does not. On 0.6.4 it cannot be combined with VLM MTP, and in these tests it did not speed up generation; if speed is what you are after, see the advanced settings. !oMLX Global Settings page: Host set to Localhost only (127.0.0.1), Port 8000, Memory Limit Total Auto (120 GB), Memory Limit Models Only Auto (108 GB), Hot Cache at 9% (12 GB), Cold Cache SSD at 10% (372 GB), Max Concurrent Requests 16, Idle Timeout None, Max Context Window 256000, Max Tokens 64000, Temperature 1.0, Top P 0.95 Model stack for multi-LLM With 128 GB you have room to load one large primary model, a fast helper, a VLM, embeddings and a reranker concurrently. Download them from Models → Downloader by pasting the Hugging Face repo URL. The recommendation holding up in May 2026 comes from people actually running Claude Code on oMLX on M5 Max (see Diego R. Baquero’s gist for the source): Unsloth’s Qwen 3.6 35B-A3B family in MoE with ~3B active parameters per token. On 128 GB the 8-bit fits without breaking a sweat: - Primary (chat + code + reasoning): unsloth/Qwen3.6-35B-A3B-MLX-8bit (~37.7 GB). MoE 35B with 3B active. The all-rounder: long chat, code, agents. On 128 GB it fits alongside everything else without squeezing. - More compressed alternative: unsloth/Qwen3.6-35B-A3B-UD-MLX-4bit (~21.6 GB) if you want two primary models loaded at once. Loses a notch of quality versus 8-bit but leaves room to experiment. - Dense reasoning (when MoE falls short): Mistral-Large-2-123B-Instruct-mlx-4bit (~70 GB) or Llama-3.3-70B-Instruct-mlx-4bit (~40 GB). For deep reasoning in a single pass, where a dense architecture beats a small-activation MoE. - Fast helper: Qwen3-14B-Instruct-mlx-4bit (~8 GB). For cheap tasks in agents, parsing and summaries. - Vision: Qwen2.5-VL-32B-Instruct-mlx-4bit (~18 GB) covers OCR, image description and multimodal reasoning. - Embeddings: BGE-M3-mlx (~1.2 GB), dense + sparse + multi-vector in a single model. - Reranker: ModernBERT-base-mlx (~150 MB) to close the loop on a decent RAG pipeline. Comfortable concurrent load with Qwen3.6 35B-A3B 8-bit as primary + 14B helper + VL-32B + embeddings + reranker: around 65-70 GB. Add Mistral Large 2 123B on top for dense reasoning and you hit ~135 GB nominal. The LRU policy moves idle models to SSD, so the cohabitation works in practice for sessions that do not pin everything at once. Pin the primary model from Model Manager so it never gets evicted. Point Claude Code at the local endpoint oMLX exposes an Anthropic-compatible API at http://127.0.0.1:8000. In current versions the routes live under /v1: POST /v1/messages and POST /v1/messages/count_tokens. Claude Code respects ANTHROPIC_BASE_URL, so pointing the CLI at your Mac is a matter of exporting environment variables. First, turn off the attribution header in Claude Code’s global config so the gateway does not bolt extra noise onto the prompt: Since 0.5.0 there is a shortcut that does all of this for you and did not exist when this recipe was written: omlx launch claude It sets ANTHROPIC_BASE_URL against your server, puts the configured key in ANTHROPIC_AUTH_TOKEN (or omlx if there is none), empties ANTHROPIC_API_KEY to force the base URL to be used, raises API_TIMEOUT_MS to 3,000,000 and disables non-essential traffic. It also sets CLAUDE_CODE_MAX_CONTEXT_TOKENS and CLAUDE_CODE_AUTO_COMPACT_WINDOW to what the engine can actually do, which is what the context-scaling option used to do by hand. It enforces a minimum window of 48,000 tokens and will not start if the chosen model falls short. There are equivalent commands for Codex, OpenCode, OpenClaw, Hermes and Pi, described in the dashboard and command line guide. The manual recipe below still works and is the one to use if you want fine control over each variable. ~/.claude/settings.json: { "env": { "CLAUDE_CODE_ATTRIBUTION_HEADER": "0" } } Then the startup command: export ANTHROPIC_BASE_URL=http://127.0.0.1:8000 export ANTHROPIC_AUTH_TOKEN=<your_api_key> export ANTHROPIC_DEFAULT_OPUS_MODEL=unsloth/Qwen3.6-35B-A3B-MLX-8bit export ANTHROPIC_DEFAULT_SONNET_MODEL=unsloth/Qwen3.6-35B-A3B-MLX-8bit export ANTHROPIC_DEFAULT_HAIKU_MODEL=Qwen3-14B-Instruct-mlx-4bit export ANTHROPIC_DEFAULT_MODEL=unsloth/Qwen3.6-35B-A3B-MLX-8bit export API_TIMEOUT_MS=600000 export CLAUDE_CODE_USE_BEDROCK=0 export DISABLE_NONESSENTIAL_TRAFFIC=1 claude --bare The --bare flag skips hooks, LSP, plugin sync and auto-memory, dropping Claude Code’s system prompt to around 1,795 tokens (versus the thousands it uses with everything loaded). For offline work against a local model that is the sensible default: every token in the system prompt is bandwidth your Mac would otherwise burn. Drop the flag when you point back at api.anthropic.com. The oMLX dashboard builds this command for you in the Claude Code with oMLX section: pick Opus, Sonnet and Haiku from three dropdowns and copy the ready-to-paste command. Set Context scaling for Claude Code to 64000: Claude Code requests 200k tokens by default, but a local model behaves better with a 64k ask than a 200k ask it cannot honor. The option scales the reported counts so auto-compact triggers at the target size. !oMLX dashboard with the Claude Code with oMLX section fully configured: Local mode active, Opus mapped to Huihui-Qwen3.6-35B-A3B-Claude-4.7-Opus-abliterated-mlx-8bit, Sonnet to Qwen3.6-40B-Claude-4.6-Opus-Deckard-Heretic-Uncensored-Thinking-8bit, Haiku to Qwen3.5-9B-Claude-4.6-HighIQ-INSTRUCT-HERETIC-UNCENSORED-MLX-mxfp8, Context scaling for Claude Code enabled with target 200000, plus pre-built commands for Codex, OpenCode and other integrations below A real caveat: Claude Code is tuned to Claude’s tool-use format and response patterns. A non-Claude model behind ANTHROPIC_BASE_URL works for autocomplete and reasoning, but you will see drops in tool-call reliability and in agentic loops. Use this for offline work, sensitive data that should not leave the Mac, or as a fallback when api.anthropic.com is rate-limiting you. It is not a 1:1 substitute for Claude Opus 4.7. SSH from another Mac If oMLX runs on a Mac Studio and you want to use Claude Code from your MacBook, SSH port forwarding is the bridge: ssh -L 8000:localhost:8000 user@mac-studio Once connected, 127.0.0.1:8000 on your laptop points to the Studio’s oMLX. Save this script as claude-local.sh: #!/bin/bash export ANTHROPIC_BASE_URL='http://localhost:8000' export ANTHROPIC_AUTH_TOKEN='' export ANTHROPIC_DEFAULT_OPUS_MODEL='Qwen3.6-35B-A3B-MLX-8bit' export ANTHROPIC_DEFAULT_SONNET_MODEL='Qwen3.6-35B-A3B-MLX-8bit' export ANTHROPIC_DEFAULT_HAIKU_MODEL='Qwen3-14B-MLX-4bit' export ANTHROPIC_DEFAULT_MODEL='Qwen3.6-35B-A3B-MLX-8bit' export API_TIMEOUT_MS=600000 export CLAUDE_CODE_USE_BEDROCK=0 export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 claude --bare --dangerously-skip-permissions Make it executable with chmod +x claude-local.sh and run it from the SSH session. The default API key is the empty field. If you set one in Settings → Auth & Info, replace '' with it. --bare drops Claude Code’s system prompt to ~1,795 tokens. --dangerously-skip-permissions bypasses interactive permission prompts that don’t make sense in an automated pipeline. Benchmark on real hardware Bench → Performance runs tests on your actual hardware. The panel covers prefill at eight prompt sizes (pp1024, pp4096, pp8192, pp16384, pp32768, pp65536, pp131072, pp200000) and continuous batching at 2x, 4x and 8x concurrency. Results go to the public oMLX leaderboard, and My Submissions shows yours next to other machines on the same profile. On 0.6.4 the upload happens automatically when each run finishes; how to avoid it is in the 0.6.4 benchmarks. Reference numbers measured on M5 Max 40-core with 128 GB on oMLX 0.3.8, in May 2026. Not extrapolated (the omlx.ai leaderboard already has M5 Max submissions, and vLLM’s independent TurboQuant study published 11 May 2026 confirms the figures for the large dense models): - Qwen 3.6 35B-A3B 8-bit (MoE, 3B active): 65-80 tok/s on decode at short context. The model that most changes day-to-day use on this machine. Live benchmarks from this setup: | Test | TTFT | Decode TPS | E2E | Peak Mem | | --- | --- | --- | --- | --- | | pp1024/tg128 | 577 ms | 72.4 tok/s | 2.35s | 38.5 GB | | pp4096/tg128 | 1,293 ms | 83.1 tok/s | 2.83s | 36.2 GB | | pp8192/tg128 | 2,995 ms | 82.1 tok/s | 4.55s | 36.5 GB | | pp32768/tg128 | 13,785 ms | 15.2 tok/s | 22.18s | 40.9 GB | Continuous batching (pp1024 / tg128): | Batch | Decode TPS | Speedup | | --- | --- | --- | | 1x (baseline) | 72.4 tok/s | 1.00x | | 2x | 87.1 tok/s | 1.20x | | 4x | 123.8 tok/s | 1.71x | | 8x | 237.7 tok/s | 3.28x | - Llama 3.3 70B 4-bit: 14-18 tok/s on decode. - Mistral Large 2 123B 4-bit with TurboQuant 3.5-bit on KV cache: 8-11 tok/s on decode. The number that matters at 128k context is peak memory, ~74 GB (FP16 KV blows past 128 GB). dasroot.net’s long-context M5 Max experiment documents the same peaks for the 104B dense model. - gpt-oss-20b MXFP4-Q4 on M5 Max 40c per the public oMLX benchmark submission lands around 100 tok/s on decode. The tables above are our own measurement on 0.3.8, and on 15 September 2026 I re-ran the tests on 0.6.4 on this same machine. With no acceleration at all, this model goes from 15.2 to 77.9 tok/s with a 32k prompt. Speculative decoding adds between 1.07x and 1.88x, depending on the model and the context. The new tables are in the 0.6.4 benchmarks. vLLM measured the TurboQuant KV-cache compression at 4.41x in May 2026. That figure is what separates "I have 128k context but it does not fit" from "I have it and can load two other models alongside it". At 4-bit and 3.5-bit, quality stays close to full precision; at 3-bit you start to feel it in code and long-form reasoning. !oMLX Bench → Performance Benchmark page with Qwen3.6-35B-A3B-MLX-8bit selected, Single Request Test checkboxes (pp1024, pp4096, pp8192, pp32768) and Continuous Batching Test checkboxes (2x, 4x, 8x), Run Benchmark button, and My Submissions link Advanced settings for maximum performance on 0.6.4 On 0.6.4, the biggest speed lever on this M5 Max is speculative decoding, and the right method depends on the model. Measured through the API at temperature 0.6, Qwen3.8-27B oQ4e went from 21.1 to 39.7 tok/s with Lightning MTP. Qwen3.6-35B-A3B 8-bit went from 79.7 to 117.5 tok/s with VLM MTP. Everything in this section was measured on 15 September 2026 on the machine this guide is about, and the method and the full tables are in the 0.6.4 benchmarks section. Which acceleration method to use with each model oMLX 0.6.4 has three speculative decoding methods and allows only one per model: - Lightning MTP: uses the multi-token prediction (MTP) head that ships inside the weights. It only works if the conversion kept the mtp.* tensors. - VLM MTP: uses an external MTP drafter under 2 GB, trained for that base model. It only kicks in when no other request is running. - DFlash: uses a z-lab block-diffusion drafter that proposes up to 16 tokens per pass. It serves requests one at a time and keeps its own prefix cache. In all three the large model verifies every proposed token, so the output comes from the large model, not from the drafter. This is what each combination measured on this M5 Max: | Model | Method | What to download | Size | Decode writing code | Decode with 15,000 tokens of context | | --- | --- | --- | --- | --- | --- | | Qwen3.6-35B-A3B 8-bit | VLM MTP | mlx-community/Qwen3.6-35B-A3B-MTP-bf16 | 1.7 GB | 79.7 → 117.5 tok/s | 84.0 → 90.7 tok/s | | Qwen3.6-35B-A3B oQ4e | Lightning MTP | Jundot/Qwen3.6-35B-A3B-oQ4e-mtp (full model) | 21.6 GB | 100.2 → 119.2 tok/s | 100.2 → 115.5 tok/s | | Qwen3.8-27B oQ4e | Lightning MTP | Jundot/Qwen3.8-27B-oQ4e-mtp (full model) | 17.0 GB | 21.1 → 39.7 tok/s | 21.4 → 27.2 tok/s | | Qwen3.8-27B 4-bit | VLM MTP | mlx-community/Qwen3.8-27B-MTP-bf16 | 0.87 GB | 24.1 → 34.5 tok/s | 21.7 → 23.2 tok/s | | Gemma 4 12B 8-bit | VLM MTP | mlx-community/gemma-4-12B-it-assistant-bf16 | 0.88 GB | 27.4 → 50.5 tok/s | 26.6 → 39.2 tok/s | | gpt-oss-20b | None | – | – | – | – | For Qwen3.6-35B-A3B, the oQ4e build with Lightning MTP was the fastest in both tests and takes about 15 GB less than the 8-bit one. With long context and prose answers every gain shrinks, because the large model accepts fewer draft tokens, but no configuration was slower than its baseline. If a Qwen model’s settings show the warning Config declares MTP layers but the weight files contain neither mtp.* tensors nor native nextn layers, the conversion stripped the MTP head. The default mlx-lm converters do that: it applied to the Qwen3.6-35B-A3B builds from mlx-community and the Qwen3.8-27B builds from lmstudio-community installed on this Mac. You have two ways out: download a build that keeps the head, such as the oQ4e-mtp builds published by the oMLX author, or turn on VLM MTP with an external drafter. Downloading the drafters Drafters download like any other model. In Models → Downloader, paste the Hugging Face repository id and start the download. oMLX recognises drafters by their type and marks them as helpers; with Hide Helper Models on, they also stay out of the API’s model list. !oMLX 0.6.4 Download from HuggingFace form with the repository id mlx-community/Qwen3.6-35B-A3B-MTP-bf16 filled in, the HF Token field empty and the Download button Turning on Lightning MTP With a model that keeps its MTP head, open Settings → Model Settings, click the model’s gear icon and turn on Lightning MTP in the Acceleration block. When you save, oMLX reloads the model. On Qwen the default is 3 draft tokens per cycle, and an adaptive controller adjusts it from each response’s acceptance rate. The oMLX log writes one line per response with that rate: on Qwen3.6-35B-A3B oQ4e it sat around 72-77%, with 2.5 to 2.9 tokens emitted per cycle. With greedy sampling the output matches the model without MTP, apart from tiny numerical differences. With temperature, rejection sampling keeps the large model’s distribution. !Acceleration block of the Jundot/Qwen3.8-27B-oQ4e-mtp model settings in oMLX 0.6.4, with the Lightning MTP switch turned on Turning on VLM MTP with an external drafter In the same Acceleration block, turn on VLM MTP, pick the drafter from the drop-down and save. Three conditions in the 0.6.4 code are worth knowing first: - The model has to load on the vision engine. The multimodal Qwen3.6 and Qwen3.8 builds do so by default, but on a text-only model the setting is silently ignored. - It cannot be combined with TurboQuant or with repetition or presence penalties. The built-in Qwen presets set presence_penalty to 1.5, which turns it off. - It only speeds up a request when nothing else is running. With concurrent requests, oMLX falls back to normal batching. !VLM MTP card in the mlx-community/Qwen3.6-35B-A3B-8bit settings in oMLX 0.6.4, turned on with the Qwen3.6-35B-A3B-MTP-bf16 drafter and the block size left empty, which means 4 tokens You can apply the same change through the admin API, which is handier when you configure more than one model or script it. The first command logs in with your key and stores the cookie; the second updates the model’s settings: curl -c omlx.cookies -X POST http://127.0.0.1:8000/admin/api/login \ -H "Content-Type: application/json" \ -d '{"api_key": "your_api_key_here"}' curl -b omlx.cookies -X PUT \ http://127.0.0.1:8000/admin/api/models/Qwen3.6-35B-A3B-8bit/settings \ -H "Content-Type: application/json" \ -d '{"vlm_mtp_enabled": true, "vlm_mtp_draft_model": "Qwen3.6-35B-A3B-MTP-bf16"}' The model name in the path and the drafter name are the ids shown in the model list. For Lightning MTP the body is {"mtp_enabled": true}. The endpoint rejects any field it does not know, so a typo does not slip through. DFlash only pays off with short prompts DFlash was the fastest method with 1,000-token prompts and the worst with long context. In the built-in benchmark, Qwen3.6-35B-A3B 8-bit rose from 92 to 112 tok/s with a 1k prompt, but dropped from 78 to 17 tok/s with 32k. Qwen3.8-27B 4-bit dropped from 23.5 to 6.3 tok/s with 32k. If you use Claude Code, which sends tens of thousands of tokens of context every turn, leave it off. For short-prompt work such as chat or autocomplete, download the matching z-lab drafter: z-lab/Qwen3.6-35B-A3B-DFlash (0.77 GB), z-lab/Qwen3.8-27B-DFlash2 (3.85 GB) or z-lab/gemma4-12B-it-DFlash (1.46 GB). Turn it on in Acceleration → DFlash and quantise the drafter to 4 bits. On the 27B, the 4-bit drafter gave 48.2 tok/s with a 1k prompt against 36.9 tok/s in bf16, and used 2.5 GiB less. It has two more limits. It serves one request at a time. And although dflash_max_ctx sends long requests to the normal engine, after the first one DFlash stays off until you reload the model. !oMLX 0.6.4 DFlash card with the Qwen3.6-35B-A3B-DFlash drafter selected, drafter quantisation on with 4-bit weights, 16-bit activations and group size 64, and the max context threshold left unlimited Prefill on the Neural Engine: no gain with Lightning MTP Qwen ANE Prompt Processing splits part of the Qwen3.5, 3.6 and 3.8 prefill between Apple’s Neural Engine (ANE) and the GPU. The Tune for this Mac button measures the best split on your machine and saves nothing until you apply the result. On Qwen3.8-27B oQ4e it recommended sending 35% of the MLP channels and 37.5% of the GDN layers to the ANE. With that split, prefill on an 8,193-token prompt went from 452 to 510 tok/s, 12.8% more. That 12.8% did not hold with the full model. With the split applied and Lightning MTP on, the built-in benchmark measured 505 tok/s of prefill with a 4k prompt against 499 without the ANE, and 422 against 471 with 32k. Through the API, the first token with 15,100 tokens of context took 34.8 s, against 35.2 s without the ANE. In exchange the loaded model grew from 16.0 to 20.7 GB and loading went from 2 to 20 s, so on this Mac I left it off. Before tuning it on an M5 Max, turn off Use both ANEs in the model settings. That mode is meant for the M3 Ultra, which joins two chips and has two ANEs. It only speeds up prompts of 2,048 tokens or more; generation stays on the GPU. The project itself documents three costs. It is not bit-exact, because the ANE runs weights requantised to INT8. It relies on private Apple interfaces that a macOS update can break, and it lengthens model load. If you turn it on anyway, also turn on Reuse Compiled ANE Programs under Global Settings → Advanced so it does not recompile on every start. !oMLX 0.6.4 Qwen ANE Prompt Processing card with the split the tuner proposed: 2048-token block, padding from 1816, 0.35 of the MLPs and 0.375 of the GDN layers on the ANE, Use both ANEs off and the Tune for this Mac button The global settings that matter oMLX’s default global configuration already suits a single user, and only four settings change anything measurable: - Kernel memory limit: macOS caps the memory the GPU can wire by default, and the dashboard shows a red warning when that cap is below what oMLX could use. Run sudo sysctl iogpu.wired_limit_mb=124518, which on 128 GB is RAM minus 5%, and restart oMLX, which applies it at start-up. The value is lost when the Mac restarts. - Prefill Priority: set it to Speed. When memory is tight, Max Context shrinks prefill down to 32 tokens per step so the prompt fits, while Speed keeps full speed and rejects what does not fit. - Hide Helper Models: turn it on so drafters stay out of /v1/models and no client picks one by mistake. - Max Concurrent Requests: keep it between 8 and 16, and do not lower it to 1. The scheduler admits no new request while as many cache writes are pending as that limit. Burst Decode can stay on Balanced. Aggressive holds tokens for up to 200 ms before sending them, against 100 ms, so the client sees the first token later. One last warning: clicking Save Settings unloads every loaded model, even if you did not touch the cache, because the dashboard resends every field. !Resource Management section of the oMLX 0.6.4 global settings: Balanced memory guard, hot cache at 9% (12 GB), SSD cache at 10% (372 GB), 16 concurrent requests, Chunked Prefill off, Prefill Priority set to Speed and no idle timeout What does not make it faster Three settings with promising names did not improve single-user speed: - TurboQuant KV: with Lightning MTP on Qwen3.8-27B oQ4e, turning it on at 4 bits left peak memory unchanged with a 32k prompt (24.4 GiB) and lowered decode from 35.4 to 30.3 tok/s. In these hybrid models only the full-attention layers keep a KV cache, so there is little to compress. It saves memory on huge contexts; it does not make generation faster. - SpecPrefill: brings the first token forward by having a small model drop the prompt tokens it scores as unimportant. The large model never sees them, so it loses information by design. - Chunked Prefill: it only cuts time to first token when requests overlap. Benchmarks on 0.6.4 on this M5 Max With 0.6.4 and the settings above, this M5 Max generates between 1.19 and 1.88 times more tokens per second when writing code. With 15,000 tokens of context, the gain sits between 1.07 and 1.47 times. Without any acceleration, Qwen3.6-35B-A3B 8-bit now holds 77.9 tok/s with a 32k prompt, where 0.3.8 dropped to 15.2 tok/s. The tests date from 15 September 2026. How it was measured There are two test sets, because the built-in benchmark shows how speed changes with context but cannot compare with and without acceleration: - Through the API: streaming requests to /v1/chat/completions at temperature 0.6 with reasoning off. There are two tests. In the short one, each configuration answers three coding tasks (Python, TypeScript and Go) twice, with 768-token answers. In the long one, it answers two questions about roughly 15,100 tokens of source code, with 512-token answers. Timing is taken on the client. - Built-in benchmark: started from POST /admin/api/bench/start, three repeats per configuration and the median, prompts of 1,025, 4,097, 16,385 and 32,769 tokens, 256 generated tokens and the code_python corpus. It always samples greedily, which is the best case for speculative decoding. Every request starts with a unique prefix, so none of them benefits from the prefix cache. Tables show the median, with the minimum and maximum in brackets when they spread more than 10% from the median in the API tests, or more than 25% in the built-in benchmark. The tests ran back to back for about three hours, with other applications open. The built-in benchmark records macOS thermal pressure, and 194 of its 242 measurements were taken at the Heavy level, the third of five (Nominal, Moderate, Heavy, Trapping and Sleeping). Only the first gpt-oss batch ran cold. Its prefill with a 16k prompt gave 3,140 tok/s cold and 1,755 tok/s hot. Read the absolute figures as those of a hot Mac. The comparisons with and without each setting were run one after the other, so the relative gains are more reliable than the absolute values. Two details of the 0.6.4 built-in benchmark change how to read it. First, when it finishes it uploads the results to omlx.ai with the chip, the models and an identifier derived from the Mac, and the dashboard has no option to prevent it. With a local model, the only way to keep the results on the Mac is to tick ANE-aligned prompts (+1 token). That is the checkbox I used, which is why the prompts are one token longer. Second, it loads vision models on the text-only engine unless MTP is on, so comparing with and without acceleration mixes two engines. With short prompts, Qwen3.6-35B-A3B oQ4e scored 49 tok/s on that engine and 100 tok/s through the API. That is why the gains in this guide come from the API tests. Speed through the API with coding answers | Model | Configuration | Decode | First token | 768-token answer | | --- | --- | --- | --- | --- | | Qwen3.6-35B-A3B 8-bit | No acceleration | 79.7 tok/s | 0.37 s | 10.0 s | | | VLM MTP | 117.5 (110.5-125.5) tok/s | 0.37 s | 6.9 s | | Qwen3.6-35B-A3B oQ4e-mtp | No acceleration | 100.2 tok/s | 0.34 s | 8.0 s | | | Lightning MTP | 119.2 (108.5-139.1) tok/s | 0.46 s | 6.9 s | | Qwen3.8-27B 4-bit | No acceleration | 24.1 tok/s | 0.79 s | 32.6 s | | | VLM MTP | 34.5 tok/s | 0.65 s | 22.8 s | | Qwen3.8-27B oQ4e-mtp | No acceleration | 21.1 tok/s | 0.82 s | 37.3 s | | | Lightning MTP | 39.7 tok/s | 0.94 s | 20.4 s | | Gemma 4 12B 8-bit | No acceleration | 27.4 tok/s | 0.84 s | 28.9 s | | | VLM MTP | 50.5 (47.5-52.7) tok/s | 0.66 s | 15.9 s | Speed through the API with 15,000 tokens of context | Model | Configuration | Decode | First token | | --- | --- | --- | --- | | Qwen3.6-35B-A3B 8-bit | No acceleration | 84.0 tok/s | 6.20 s | | | VLM MTP | 90.7 tok/s | 5.96 s | | Qwen3.6-35B-A3B oQ4e-mtp | No acceleration | 100.2 tok/s | 5.83 s | | | Lightning MTP | 115.5 tok/s | 6.14 s | | Qwen3.8-27B 4-bit | No acceleration | 21.7 tok/s | 34.5 s | | | VLM MTP | 23.2 tok/s | 32.6 s | | Qwen3.8-27B oQ4e-mtp | No acceleration | 21.4 tok/s | 31.3 s | | | Lightning MTP | 27.2 (24.4-30.1) tok/s | 35.2 s | | Gemma 4 12B 8-bit | No acceleration | 26.6 tok/s | 20.1 s | | | VLM MTP | 39.2 tok/s | 19.3 s | Each row is the median of two 512-token answers over the same code context. With this context the large model accepted fewer draft tokens than when writing code: on Qwen3.8-27B 4-bit, the VLM MTP gain fell from 1.43x to 1.07x. Built-in benchmark by prompt length Generation speed in tok/s by prompt length, with prefill at 4k and MLX peak memory, which includes the weights: | Model and configuration | 1k | 4k | 16k | 32k | Prefill 4k | Peak | | --- | --- | --- | --- | --- | --- | --- | | Qwen3.6-35B-A3B 8-bit, no acceleration | 92.1 | 91.1 | 84.1 | 77.9 | 2,426 | 38.7 GiB | | Qwen3.6-35B-A3B 8-bit, VLM MTP | 118.3 | 106.6 | 100.2 | 94.4 | 3,113 | 41.8 GiB | | Qwen3.6-35B-A3B 8-bit, DFlash (4-bit drafter) | 112.4 | 110.5 | 32.6 (23.3-35.0) | 16.6 | 2,734 | 38.2 GiB | | Qwen3.6-35B-A3B oQ4e-mtp, no acceleration | 49.3 | 46.1 | 45.2 | 42.2 | 2,258 | 23.2 GiB | | Qwen3.6-35B-A3B oQ4e-mtp, Lightning MTP | 77.2 (47.0-101.7) | 85.8 | 87.9 (85.7-116.6) | 82.8 (54.7-99.5) | 2,014 | 24.6 GiB | | Qwen3.6-35B-A3B oQ4e-mtp, DFlash (4-bit drafter) | 142.7 | 86.3 | 39.2 | 17.3 | 3,373 | 22.7 GiB | | Qwen3.8-27B 4-bit, no acceleration | 30.6 | 27.3 | 25.0 | 23.5 | 525 | 21.7 GiB | | Qwen3.8-27B 4-bit, VLM MTP | 39.3 | 36.6 | 36.4 | 30.9 | 619 | 25.4 GiB | | Qwen3.8-27B 4-bit, DFlash2 (4-bit drafter) | 48.2 (38.7-51.1) | 43.7 | 35.8 (22.0-39.0) | 6.3 | 561 | 22.8 GiB | | Qwen3.8-27B 4-bit, DFlash2 (bf16 drafter) | 36.9 | 35.1 | 22.3 | 5.9 | 568 | 25.3 GiB | | Qwen3.8-27B oQ4e-mtp, no acceleration | 27.0 | 26.4 | 24.4 | 21.4 | 545 | 22.2 GiB | | Qwen3.8-27B oQ4e-mtp, Lightning MTP | 48.0 (42.6-58.6) | 37.7 | 41.1 | 35.4 | 499 | 24.4 GiB | | Qwen3.8-27B oQ4e-mtp, Lightning MTP + TurboQuant 4-bit | 42.2 | 37.7 | 39.7 | 30.3 | 547 | 24.4 GiB | | Qwen3.8-27B oQ4e-mtp, Lightning MTP + ANE prefill | 35.6 | 33.1 | 35.8 | 30.9 | 505 | 28.3 GiB | | Qwen3.8-27B oQ4e-mtp, DFlash2 (4-bit drafter) | 41.4 | 33.8 | 18.8 (16.6-21.9) | 5.8 | 458 | 23.3 GiB | | Gemma 4 12B 8-bit, no acceleration | 31.0 | 30.5 | 30.1 | 28.8 | 955 | 14.5 GiB | | Gemma 4 12B 8-bit, VLM MTP | 65.6 | 42.7 | 50.6 | 49.0 | 940 | 15.5 GiB | | Gemma 4 12B 8-bit, DFlash (4-bit drafter) | 92.4 (35.5-158.0) | 34.4 | 53.4 | 51.2 | 997 | 18.1 GiB | | gpt-oss-20b, no acceleration | 120.2 | 106.5 | 100.4 | 78.8 | 3,491 | 12.4 GiB | DFlash figures with a 1k prompt varied widely between repeats: on Gemma 4 12B they were 35.5, 92.4 and 158.0 tok/s. Speed depends on how many draft tokens the large model accepts, and that changes with the text it has to continue. Compared with 0.3.8 The May table and this one are not fully comparable. That one was measured on the Qwen3.6-35B-A3B-MLX-8bit copy with 128 output tokens; this one, on the mlx-community copy with 256. Even so, the long-context improvement is too large to come from those differences: | Qwen3.6-35B-A3B 8-bit, built-in benchmark | 0.3.8 (May) | 0.6.4 (September) | | --- | --- | --- | | Decode with a 1k prompt | 72.4 tok/s | 92.1 tok/s | | Decode with a 4k prompt | 83.1 tok/s | 91.1 tok/s | | Decode with a 32k prompt | 15.2 tok/s | 77.9 tok/s | | First token with a 1k prompt | 577 ms | 298 ms | End-to-end verification A curl call to the OpenAI-compatible API to confirm the server is alive and a model responds: curl http://127.0.0.1:8000/v1/chat/completions \ -H "Authorization: Bearer your_api_key_here" \ -H "Content-Type: application/json" \ -d '{ "model": "Qwen3-Coder-30B-A3B-Instruct-mlx-8bit", "messages": [{"role": "user", "content": "Hello from oMLX"}] }' And from Claude Code, once you have exported the variables from the previous section: claude --print "Are you running locally?" The reply should come back from local without touching api.anthropic.com. To confirm at the network level, open Activity Monitor → Network, filter by the claude process and check that the outbound connection is against 127.0.0.1:8000. !oMLX Chat view at localhost:8000/admin/chat with the model dropdown open, listing the four chat-completion models available locally (both Huihui-Qwen3.6-35B-A3B-Claude-4.7-Opus variants, Qwen3.5-9B-Claude-4.6-HighIQ, and Qwen3.6-40B-Claude-4.6-Opus-Deckard-Thinking); the embeddings and reranker models do not appear here because they are not chat-completion What has changed since 0.3.8 This guide was written against oMLX 0.3.8 in May 2026. The current version is 0.6.4, released on 29 August 2026, and more than a dozen releases in between touch exactly what is tuned here. What follows is the summary of what changed, with the figures attributed to whoever measured them. Lightning MTP speculative decoding This is the change that affects this configuration most. Version 0.5.0, from 10 July 2026, added native depth-k speculative decoding for Qwen3.6-27B, Qwen3.6-35B-A3B, DeepSeek-V4-Flash and GLM-5.2. On an Apple M3 Ultra the project measured Qwen3.6-35B-A3B going from 89.6 to 140.4 tok/s, a 1.57x gain. Qwen3.6-27B went from 35.0 to 55.1 tok/s. On this M5 Max, measured through the API on 15 September 2026, Lightning MTP took Qwen3.8-27B oQ4e from 21.1 to 39.7 tok/s. How to turn it on, and when another method is the better choice, is in the advanced settings. Custom kernels, with a note for M5 The same 0.5.0 added custom compute kernels for DeepSeek V4, Qwen3.5/3.6 and GLM-5.2, with prefill improvements measured on M3 Ultra. They are 45% on DeepSeek-V4-Flash (313.3 to 455.8 tok/s at 64k context), 33% on Qwen3.6-27B and 99% on GLM-5.2 at 32k. What matters for this machine is that dispatch is NAX-aware precisely to avoid regressions on the M5 series, so it is worth confirming the effect rather than assuming it. oQ and oQe quantisation oMLX no longer depends only on the weights you download: it brings its own quantisation scheme. Version 0.5.0 added oQe, an activation-importance calibration pass with expert-coverage tracking for MoE models. In the project’s measurements oQ4e improves average accuracy over oQ4 while staying in the same disk-size class. That is 83.88% against 82.83% on Qwen3.6-35B-A3B, and 73.09% against 70.01% on Qwen3.5-9B. The neural engine and cold starts The 0.6.3 branch worked on the prefill path over Apple’s neural engine. Two of the project’s figures matter day to day. Bank compilation memory dropped from 35.8 GB to 4.7 GB, and an optional persistent compile cache cuts fresh-process startup by 53% to 66%. Version 0.6.4 continued down that road and improved Qwen3.8-Flash-Next prompt processing by 33.5%, with 24.2% less total request time at 32k, again on M3 Ultra. New endpoints The OpenAI- and Anthropic-compatible pair has been joined by /v1/rerank for document reranking, /v1/responses for Codex compatibility and /v1/mcp/tools for Model Context Protocol. With an embedding model and a reranker loaded at once, a whole retrieval pipeline fits on the same server. The full breakdown is in the guide to the API, the key and the port. Sub-key authentication Where there used to be a list of equivalent keys, there are now three credentials with different powers. A main key opens the API and the panel; sub-keys only call the API and cannot log into the panel or change settings; panel session tokens are held in a signed cookie. If you are handing access to more than one application, hand out sub-keys. The setting names The memory configuration in the earlier section still holds as judgement, but the keys are named differently in ~/.omlx/settings.json. The memory ceiling is now memory_guard_tier, with four levels (safe, balanced, aggressive and custom), and the bespoke value goes in memory_guard_custom_ceiling_gb. By default the ceiling is system RAM minus 8 GB. The two cache tiers are hot_cache_max_size and ssd_cache_max_size, and the concurrency limit is max_concurrent_requests. Watch one of them: hot_cache_max_size ships as "0", which disables the hot cache in RAM. I go through it in benchmarks and the memory ceiling. Other pieces that did not exist Profiles save named bundles of settings over a single model, exposed as model:profile and at no extra memory cost. Aliases rename a model as far as the API is concerned. And there is experimental distributed inference, splitting a model’s layers across more than one Mac over SSH according to each machine’s memory, accounting for whether the link is Thunderbolt or 10GbE. The first two are in the dashboard and command line guide. Sources and further reading - Diego R. Baquero, "Running Claude Code with a local LLM": the tested configuration recipe this post’s settings are based on (TurboQuant 3.5-bit, 64k context, attribution off, Qwen 3.6 35B-A3B models). - A First Comprehensive Study of TurboQuant, vLLM blog, 11 May 2026: independent analysis confirming 3.5-bit matches full-precision quality. - omlx.ai: public leaderboard with real submissions from M5 Max and other machines. - Maxing Out M5 Max Context Windows: Memory Fragmentation and TurboQuant Benchmarks (dasroot.net, April 2026): the other detailed experiment on long-context M5 Max with TurboQuant. - Repositories: jundot/omlx, ml-explore/mlx, ml-explore/mlx-lm, huggingface.co, huggingface.co. If you came here wondering what this actually is and how it differs from Apple’s MLX framework, I separate the two in what is oMLX. For a cross-platform alternative outside Apple Silicon, the Ollama local LLM guide covers the model catalogue and OpenAI-compatible API. To wire the local endpoint into an agent, the Anthropic SDK tutorial and the MCP multi-vendor patterns already talk to this same format with no code changes. Related resources - Calculator: which LLM fits your GPU? - Learning path: Local LLMs Frequently asked questions Do I need to set an API key to use oMLX locally only? No. oMLX ships with the API key field empty, and as long as the instance only listens on 127.0.0.1 that is safe for local testing. The moment you expose it to the LAN or share the endpoint, go to Settings → Auth & Info and set a long random string. Current versions also offer sub-keys that can only call the API and cannot log into the panel or change settings, meant for handing access to more than one application. Can I use Claude Code from a MacBook against oMLX running on a Mac Studio? Yes, through SSH port forwarding: ssh -L 8000:localhost:8000 user@mac-studio makes 127.0.0.1:8000 on the laptop point at the Studio's oMLX. Then export the same variables (ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN and the ANTHROPIC_DEFAULT_*_MODEL set) in a script such as claude-local.sh, make it executable with chmod +x and run claude --bare from the SSH session. If you set a key in Settings → Auth & Info, replace the empty string with it. Are the benchmarks in this post still valid if I install 0.6.4? The May tables, no: they were measured on 0.3.8, and when I re-ran them on 15 September 2026, 0.6.4 was faster on this same machine. Without acceleration, Qwen3.6-35B-A3B 8-bit goes from 15.2 to 77.9 tok/s with a 32k prompt. Lightning MTP or VLM MTP add between 1.07x and 1.88x, depending on the model and the context. If you measure on your own Mac, compare through the API and not only with Bench → Performance, which runs vision models on a different engine when MTP is off. --- Originally published on jacar.es, where the benchmarks are kept up to date with each oMLX release.
Qwen3.8 vs Ornith 1.5 on a Mac: speed and code review On a Mac Studio M4 Max with 64 GB, Qwen3.8-27B with MTP does not replace the 35B mixture-of-experts model I already run. Measured on 12 October 2026 under oMLX 0.7.0, Qwen generated 55 tokens per second against 90, and took 3.46 seconds to produce its first token on an 800-token prompt against 0.49. On a code review test the two caught the same bugs with reasoning off, and Qwen caught fewer with it on. The number that decides it is time to first token Qwen3.8-27B needs seven times longer than Ornith 1.5 to start answering an 800-token prompt: 3.46 seconds against 0.49, averaged over 8 requests per model. Output speed is closer, 55.1 tokens per second against 90.4. In a chat that gap is a nuisance. A coding agent or a code review sends long prompts many times in a row, and there the wait before the first token is most of the cost. oMLX is an inference server for Apple Silicon Macs that serves MLX models behind an OpenAI-compatible API. I run both models on it, and each one has its own trick for generating faster. Qwen3.8-27B is a dense model: all 27 billion parameters work on every token. It ships with MTP, multi-token prediction, an extra head trained with the model that drafts the next few tokens so the model can verify them in one pass. Ornith 1.5 35B-A3B is a mixture of experts: 35 billion parameters, about 3 billion active per token. I run it with DFlash, where a separate small draft model proposes a block of tokens and the main model checks them. The chart puts both measurements side by side. The first pair of bars is the one I care about. !Bar chart of two measurements on oMLX 0.7.0. Time to first token on an 800-token prompt: Ornith with DFlash 0.49 seconds, Qwen3.8-27B with MTP 3.46 seconds. Output speed: Ornith 90.4 tokens per second, Qwen 55.1. Ornith wins both. I measured with aiperf 0.12.0, one model loaded at a time, the server restarted before each run and no other traffic. This is the command. It works against any OpenAI-compatible server: aiperf profile --url http://localhost:8010 --model <model-id> \ --endpoint-type chat --streaming \ --concurrency 1 --request-count 8 --num-warmup-requests 2 \ --prompt-input-tokens-mean 800 --prompt-input-tokens-stddev 0 \ --output-tokens-mean 400 --output-tokens-stddev 0 \ --extra-inputs '{"temperature":0.6,"chat_template_kwargs":{"enable_thinking":false}}' \ --tokenizer <model-snapshot-dir> --random-seed 7 Upgrading oMLX helped Qwen by about 20%, and it was not enough Moving from oMLX 0.6.4 to 0.7.0 raised Qwen3.8-27B from 57-59 to 71.7 tokens per second on a code prompt and from 46-48 to 55-56 on prose, two runs per cell with 400 output tokens. The oMLX 0.7.0 release notes list faster MTP verify and decode for this model, and the numbers agree. Ornith stayed where it was: 103-121 tokens per second on code, 99-102 on prose. I also tried the obvious knob. MTP drafts up to 4 tokens per cycle by default on this model, and oMLX lets you raise the ceiling. At 6 the output speed was 53.9 tokens per second, at 8 it was 52.4, against 55.1 at the default. The adaptive controller already picks a depth that pays, so I put the setting back. On a code review test the two models tie With reasoning off, Qwen3.8-27B caught 9 of 20 planted bugs and Ornith 1.5 caught 8. With 20 cases, one bug of difference is noise. The test takes 20 real commits from four of my repositories, flips one operator on an added line (< to <=, == to !=, a dropped not) and asks the model to review the diff in a single call, without tools. Each case is paired with the clean diff, and a model only gets credit if it flags the planted line on the mutant and not on the clean version. | Model | Reasoning | Bugs caught | Unreadable answers | Findings per clean diff | Median time | |---|---|---|---|---|---| | Qwen3.8-27B + MTP | off | 9 of 20 (45%) | 4 of 40 | 0.7 | 10 s | | Ornith 1.5 + DFlash | off | 8 of 20 (40%) | 2 of 40 | 1.6 | 3 s | | Qwen3.8-27B + MTP | on | 6 of 20 (30%) | 12 of 40 | 0.3 | 194 s | | Ornith 1.5 + DFlash | on | 8 of 20 (40%) | 13 of 40 | 0.0 | 102 s | The Ornith rows were measured on 2 October 2026 on oMLX 0.6.4, the Qwen rows on 12 October on 0.7.0. With reasoning off, where they differ is false alarms. Qwen reports half as many problems on code that has none, and it takes three times as long per review. Turning reasoning on does not help Qwen. It caught 6 of 20 against 9 without it, and half of its 40 answers used up the whole 12,000-token output limit, which is why 12 of them never reached valid JSON. Ornith overthinks in the same way and loses 13 of 40 answers, but it holds at 8 of 20 and takes about half the time per review. A review that needs three minutes and fails to parse a third of the time is no use to me from either model. The two models do not fit in memory together Qwen3.8-27B takes 16.1 GB once loaded and Ornith 1.5 takes 19.8 GB. On a 64 GB machine that sounds fine, and under oMLX 0.6.4 both did load, with swap climbing to 15.3 of 16 GB. The memory guard in 0.7.0 refuses instead. While Qwen was busy, every request for Ornith got this: POST /v1/responses → 507: Cannot load Ornith-1.5-35B-A3B-MLX-4bit: projected memory 38.24GB would exceed the dynamic memory ceiling 34.51GB When Qwen is idle, oMLX evicts one model to load the other, and the log marks the unload as an immediate abort, so a request in flight on the evicted model is lost. I found out what that looks like under load by accident. I ran the review test on Qwen with reasoning on for 90 minutes while another tool kept sending requests to Ornith. oMLX loaded Ornith, evicted it six seconds later to make room for Qwen's prompt, loaded it again, and eventually evicted Qwen in the middle of an answer. Of Qwen's 40 requests, 19 failed. I had hoped to keep Qwen beside Ornith as a second opinion, and that is off the table. I have to pick one, and on these numbers I pick Ornith. Qwen sometimes cuts its own JSON, and the fix costs half the speed In 5 of 40 review answers Qwen3.8-27B returned {"findings":[] and stopped, with the closing brace missing. It only happened on empty answers of five or six tokens. I reran the five affected diffs three times each under four settings, with MTP on and off and with the presence penalty at 1.5 and 0. The cut answers appeared with MTP off too, and without the penalty, in up to 4 of 15 runs. As far as I can tell the model just ends early now and then. Constrained decoding removes it. oMLX has it built in through xgrammar, but the Homebrew formula leaves it out unless you ask: brew reinstall jundot/omlx/omlx --with-grammar With a JSON schema in the request, 0 of 30 answers were unparseable. The diagram shows what that costs. A grammar and MTP cannot run together, so those requests fall back to plain decoding at about 25 tokens per second. !Flow diagram of a review request to Qwen3.8-27B. Without a JSON schema it uses MTP decoding at about 55 tokens per second and up to 4 of 15 answers are unparseable. With a JSON schema it uses grammar-constrained decoding with MTP switched off at about 25 tokens per second and 0 of 30 answers are unparseable. What is still open Qwen's row with reasoning on mixes two runs. 21 answers come from the morning run described above, with other traffic on the server, and I repeated the 19 failed requests in the afternoon with everything else paused. I counted the morning answers because they were complete, but I did not rerun all 40 in one clean pass. The speed test used 8 requests and one prompt size. I did not test Qwen's vision, and I do not know what a JSON schema costs Ornith with DFlash. I removed Qwen from the proxy that evening and deleted its weights, so none of that will be measured. The next candidate is Muse Glimmer 30B, which is being measured now. For interactive work, where you wait on every answer, Ornith stays. If you are choosing a local model for a Mac, measure time to first token at the prompt length you really send before you look at tokens per second. For unattended jobs that gap matters much less.