Когда я впервые попытался учесть время локального прогона 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. Сама задача — это фоновый процесс. С точки зрения ОС вы всё ещё в терминале, даже если ушли час назад.

Если затем вы закрываете крышку или отходите, происходят две вещи:

  1. macOS считает вас бездействующим (нет ввода дольше 90 секунд).
  2. macOS считает систему потенциально готовой ко сну — в зависимости от настроек питания и того, закрыта ли крышка.

Первое заставляет ваш трекер времени перестать считать (правильно — вы не работаете). Второе заставляет вашу модель перестать работать (неправильно — она работала).

Решение второго — то, о чём я писал в Запускайте Ollama в режиме закрытой крышки. Удерживаете power assertion — Mac не засыпает, модель продолжает генерировать. Timex упаковывает это в переключатель режима закрытой крышки, чтобы вам не нужно было помнить про флаг caffeinate.

Решение первого — тема этого поста.

Что вы на самом деле хотите измерять в ML-работе

Есть три разные длительности, которые могут вас интересовать для прогона LLM/ML:

  1. Время по часам — когда прогон начался, когда закончился, сколько всего прошло.
  2. Время вычислений — когда модель активно работала (GPU/CPU > 0), а не ждала вас и не зависла.
  3. Время вашего внимания — когда вы реально наблюдали за прогоном / контролировали его.

Для задачи квантизации вас в основном интересуют #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 утром скажет вам, сработало ли.