Когда я впервые попытался учесть время локального прогона LLM, я его потерял.
В субботу утром я запустил 3-часовую Q4-квантизацию модели на 7B. Запустил таймер в Toggl. Ушёл. Вернулся к обеду — а там спящий Mac, незавершённая задача и таймер Toggl, бодро записавший 3 часа 14 минут на проект «ml-experiments».
Данные были ложью. Работа не происходила. Mac уснул через 20 минут; квантизация остановилась; график загрузки CPU за эти 3 часа был ровной линией. Но Toggl было всё равно — я сказал ему, что работаю, и он засчитал меня работающим.
Это структурная проблема учёта времени на локальной AI/ML-работе. То, что использует ваш компьютер, — это не вы. Ваш трекер времени измеряет то, что делаете вы, а узнать вы на самом деле хотите продвигается ли модель. Это разные сигналы.
К этому моменту я провёл достаточно времени, гоняя файн-тюны, оценки и генерации с длинным контекстом локально на Apple Silicon, чтобы рабочий процесс устоялся в несколько паттернов. Вот что я понял о том, как учитывать это правильно — и что Timex фиксирует, а другие инструменты упускают.
Как «модель работает» на самом деле выглядит для Mac
Когда Ollama, LM Studio, MLX или llama.cpp делают работу, приложением на переднем плане у вашего Mac остаётся то, на что вы последним кликнули. Обычно это терминал, из которого вы запустили задачу, или окно чата LM Studio, или вкладка браузера, нацеленная на localhost:11434. Сама задача — это фоновый процесс. С точки зрения ОС вы всё ещё в терминале, даже если ушли час назад.
Если затем вы закрываете крышку или отходите, происходят две вещи:
- macOS считает вас бездействующим (нет ввода дольше 90 секунд).
- macOS считает систему потенциально готовой ко сну — в зависимости от настроек питания и того, закрыта ли крышка.
Первое заставляет ваш трекер времени перестать считать (правильно — вы не работаете). Второе заставляет вашу модель перестать работать (неправильно — она работала).
Решение второго — то, о чём я писал в Запускайте Ollama в режиме закрытой крышки. Удерживаете power assertion — Mac не засыпает, модель продолжает генерировать. Timex упаковывает это в переключатель режима закрытой крышки, чтобы вам не нужно было помнить про флаг caffeinate.
Решение первого — тема этого поста.
Что вы на самом деле хотите измерять в ML-работе
Есть три разные длительности, которые могут вас интересовать для прогона LLM/ML:
- Время по часам — когда прогон начался, когда закончился, сколько всего прошло.
- Время вычислений — когда модель активно работала (GPU/CPU > 0), а не ждала вас и не зависла.
- Время вашего внимания — когда вы реально наблюдали за прогоном / контролировали его.
Для задачи квантизации вас в основном интересуют #1 и #2 (была ли она эффективной, не засыпал ли Mac всё это время). Для интерактивной сессии оценки вас в основном интересует #3 (сколько вашего времени съел этот эксперимент). Чтобы решить, оставлять ли модель в продакшене, вас интересуют все три.
Большинство универсальных трекеров времени смешивают их. Toggl скажет вам, что вы провели 3 часа на «ml-experiments», если вы запустите таймер и уйдёте. RescueTime скажет вам, что вас не было за компьютером. Оба технически правы и по отдельности бесполезны.
Вам нужна запись о том, когда прогон начался, когда закончился и что вы делали в этом окне. Timex фиксирует все три в одном логе.
Как Timex видит локальный прогон LLM
Вот реальная форма данных для Q4-квантизации, которую я гонял на прошлой неделе. Прогон занял 2 часа 41 минуту по часам. За клавиатурой из них я был 22 минуты.
-- The 3-hour window of the quantization run
SELECT
ts,
app_name,
window_title,
is_idle
FROM activity_samples
WHERE ts BETWEEN strftime('%s', '2026-05-31 09:14:00')
AND strftime('%s', '2026-05-31 11:55:00')
ORDER BY ts;
Примерная форма строк (выборка каждые 15 минут для читабельности):
09:14 Ghostty llama.cpp quantize-stats ... 0 (active, just started)
09:29 Ghostty llama.cpp quantize-stats ... 0 (active, watching output)
09:44 Ghostty llama.cpp quantize-stats ... 1 (idle, walked away)
09:59 loginwindow — 1 (locked screen)
10:14 loginwindow — 1 (still locked)
... [no rows from 10:30 to 11:30 — display slept, sampler paused]
11:32 Ghostty llama.cpp quantize-stats ... 0 (returned, woke up Mac)
11:47 Ghostty llama.cpp quantize-stats ... 0 (watching final phases)
11:55 Ghostty llama.cpp quantize-done 0 (job finished)
Несколько вещей, на которые стоит обратить внимание.
Заголовок окна всё время одна и та же строка (llama.cpp quantize-stats ...). Это важно — значит, позже я могу грепнуть файл SQLite и найти каждый прогон квантизации по заголовку окна. Терминал сохраняет имя процесса переднего плана в строке заголовка; это та хлебная крошка, что мне была нужна.
Строки пропадают во время окна сна дисплея (с 10:30 до 11:30). Это правильно. Сэмплер не должен заявлять об активности, когда ничего не записывается. Пробел в полосе честен — он показывает, что Mac спал, — и этот пробел также то, что я захочу позже расследовать (был это сбой режима закрытой крышки или я забыл его включить?).
Строки экрана блокировки между ними (loginwindow) — диагностическое золото. Они говорят мне, что экран заблокировался в 09:59 — тогда я и вышел из комнаты. Если бы я переживал, реально ли прогон шёл, пока меня не было, отсутствие строк краша или строк другого приложения успокаивает.
«Сработал ли режим закрытой крышки в этот раз?»
Это вопрос, который я задаю о своих ML-прогонах чаще всего. Не засыпал ли Mac всё это время? Или режим закрытой крышки в какой-то момент дал сбой?
SQL-запрос даёт ответ:
-- For a known run window, find any gap > 5 minutes (= sleep event)
WITH sample_gaps AS (
SELECT
ts AS t,
ts - LAG(ts) OVER (ORDER BY ts) AS gap_seconds
FROM activity_samples
WHERE ts BETWEEN strftime('%s', '2026-05-31 09:14:00')
AND strftime('%s', '2026-05-31 11:55:00')
)
SELECT datetime(t, 'unixepoch', 'localtime') AS gap_started, gap_seconds
FROM sample_gaps
WHERE gap_seconds > 300
ORDER BY t;
Если режим закрытой крышки сработал идеально, этот запрос возвращает ноль строк. Если Mac в какой-то момент уснул, вы получаете строку на каждое окно сна со временем начала и длительностью.
Для прогона выше ответ был: один пробел, 53 минуты. Mac спал с 10:30 до 11:23. Режим закрытой крышки был выключен. Я забыл его включить. Квантизация встала на час, проснулась, когда я вернулся и подвигал мышью, и продолжилась.
Весь прогон занял на 1 час 53 минуты больше, чем должен был. Это вопрос, который нельзя задать без трекера, записывающего честные пробелы.
Аннотирование прогонов, не покидая терминал
Простейший механизм «для чего был этот прогон» — заголовок окна терминала. Большинство терминалов позволяют задать заголовок ANSI-escape-последовательностью:
echo -ne "\033]0;quant-7b-q4 — eval-set-3\007"
ollama run mistral:7b-instruct-q4 < eval-set-3.jsonl > out.jsonl
Это устанавливает заголовок окна в «quant-7b-q4 — eval-set-3» на время выполнения команды. Timex фиксирует этот заголовок в каждой строке выборки. Позже вы можете грепнуть eval-set-3 и вытащить точное окно того эксперимента.
Приём обобщается:
# Wrap any long-running ML command with a title tag
run_titled() {
local title="$1"
shift
echo -ne "\033]0;${title}\007"
"$@"
echo -ne "\033]0;${SHELL##*/}\007"
}
run_titled "mlx-finetune-llama3-r1-lr2e5" python finetune.py --lr 2e-5
Теперь каждая строка выборки внутри этого окна показывается как mlx-finetune-llama3-r1-lr2e5 в заголовке окна, и вы можете вытащить точное время начала/конца и паттерн простоя из SQLite спустя недели. Это самая дешёвая форма учёта экспериментов, какую можно вообразить, и она живёт без всякого облачного сервиса.
Реальная ML-неделя
Вот как работа прошлой недели с заголовками mlx-* и llama-* выглядела в данных:
SELECT
window_title,
date(MIN(ts), 'unixepoch', 'localtime') AS started,
ROUND((MAX(ts) - MIN(ts)) / 60.0, 1) AS wall_minutes,
ROUND(SUM(CASE WHEN is_idle = 0 THEN 1 ELSE 0 END) / 60.0, 1) AS active_minutes
FROM activity_samples
WHERE window_title LIKE 'mlx-%' OR window_title LIKE 'llama-%'
GROUP BY window_title
ORDER BY MIN(ts);
Вывод:
mlx-finetune-llama3-r1-lr2e5 2026-05-26 189.2 wall 34.1 active
llama-cpp-quant-7b-q4-eval3 2026-05-31 161.8 wall 22.4 active
llama-cpp-quant-7b-q8-eval3 2026-06-01 178.5 wall 18.9 active
mlx-eval-mtbench-r1-vs-base 2026-06-02 87.0 wall 71.2 active
Оценка (последняя строка) — 87 минут по часам и 71 минута активности — это была интерактивная сессия скоринга, где я смотрел выводы в реальном времени. Две задачи квантизации — 18–22 минуты моего внимания на 2,5–3 часа по часам — длинные прогоны без присмотра.
Это соотношение важно. Колонка «активность против часов» говорит мне, какие эксперименты стоили моего времени, а какие — моего электричества. Из этого следуют разные решения.
Чем вы жертвуете по сравнению с «настоящим» трекером ML-экспериментов
Вот чем Timex является и чем не является для ML-процесса.
Специализированный трекер экспериментов — Weights & Biases, MLflow, Aim — отслеживает модель. Кривые потерь, гиперпараметры, метрики, артефакты. Ничего из этого в Timex не живёт. Если вам это нужно, гоняйте W&B рядом; они дополняют друг друга.
Timex отслеживает человеческую сторону прогона. Когда вы его запустили. Когда отошли. Когда он закончился. Что вы делали в это время. Не засыпал ли Mac. Заглядывали ли вы в Twitter, пока он шёл. Ничего из этого не живёт в W&B.
Если вы занимаетесь серьёзным ML на своём Mac и не логируете никаких данных «человеческой стороны» — а большинство не логируют — Timex это самый дешёвый способ начать. Данные в файле SQLite, который вы и так понимаете. Запросы короткие. График — это полоса в виде «Сегодня».
И с практической стороны: режим закрытой крышки — это та функция, которая делает ночные прогоны жизнеспособными на ноутбуке. Если вы хоть раз просыпались к обучению, готовому на 30%, которое должно было быть готово на 100%, вы уже понимаете почему.
Запустите следующую ночную задачу с включённым режимом закрытой крышки Timex. Лог SQLite утром скажет вам, сработало ли.