← к витрине

Витрина TAKT: прогнозирование временных рядов на Rust (DLinear и PatchTST на ETT)

Две стандартные модели долгосрочного прогнозирования, DLinear (LTSF-Linear) и PatchTST, обученные официальными скриптами на наборах данных ETT, переведены с PyTorch на чистый Rust (без BLAS и без ML-рантайма) и собраны дважды: исходная сборка этого кода на Rust и сборка TAKT того же кода. Прогнозы обеих сборок совпадают до бита. Здесь — готовые программы обеих сборок, исходный код исходной версии, скрипт, который воссоздаёт входные окна из официальных данных, скрипт бенчмарка и наши замеры.

Что вычисляют программы

Каждое окно тестовой части набора данных ETT (официальные границы, features M, StandardScaler, обученный на обучающей части): на входе 336 прошлых шагов x 7 каналов, на выходе следующие 96 шагов x 7 каналов, float32. ETTh1/ETTh2: 2 785 окон; ETTm1/ETTm2: 11 425 окон. Веса чекпойнта для выбранного набора данных вкомпилированы в программу (--data).

Модель Набор данных MSE / MAE на тесте (все окна, tools/score.py) Официальный прогон обучения, тест
DLinear ETTh1 0,384144 / 0,404713 0,384144 / 0,404713
DLinear ETTh2 0,290098 / 0,353328 0,290098 / 0,353328
DLinear ETTm1 0,301222 / 0,344619 0,301222 / 0,344619
DLinear ETTm2 0,171852 / 0,267122 0,171852 / 0,267122
PatchTST ETTh1 0,385126 / 0,405953 (первые 2 688: 0,381618 / 0,405088) 0,381618 / 0,405088
PatchTST ETTh2 0,274616 / 0,337234 (первые 2 688: 0,274118 / 0,336000) 0,274117 / 0,336000

Официальный тестовый цикл PatchTST отбрасывает последний неполный батч из 128 окон, отсюда значения «первые 2 688». Перевод на Rust не совпадает с PyTorch бит в бит (другой порядок суммирования; наибольшая абсолютная разница с PyTorch eager — от 2e-6 до 2e-5); сборка TAKT совпадает с исходной версией на Rust бит в бит.

Замер (стенд TAKT)

Дата замера: 3 октября 2026 г. AMD Threadripper PRO 5975WX (Zen 3), Linux. Зарезервирован один CCX из 8 ядер, их SMT-соседи простаивают; однопоточные прогоны привязаны к одному ядру, восьмипоточные — к 8 ядрам этого CCX. Для каждого набора данных: одна прогревочная пара прогонов, затем 9 раундов с чередованием порядка, медиана. Время: цикл прогнозирования, засекаемый самой программой (чтение окон и запись прогнозов не учитываются: в обеих сборках они стоят одинаково). Такты: такты пользовательского режима того же цикла (perf stat, сумма по всем потокам). Обе сборки используют один и тот же драйвер, который, как долгоживущий сервис, оставляет освобождённую память кучи в процессе (glibc mallopt), чтобы временные буферы на каждое окно не превращались в page faults. Замерены ровно файлы из bin/, скриптом tools/run_bench.sh.

Модель Набор данных Окна Потоки Исходная, мс TAKT, мс Ускорение Исходная, млн тактов TAKT, млн тактов Ускорение по тактам Совпавшие раунды
DLinear ETTh1 2 785 1 788,4 111,0 7,10× 3 518 486 7,23× 9/9
DLinear ETTh2 2 785 1 785,0 108,4 7,24× 3 512 476 7,38× 9/9
DLinear ETTm1 11 425 1 3 223,9 447,8 7,20× 14 421 1 978 7,29× 9/9
DLinear ETTm2 11 425 1 3 228,4 442,6 7,29× 14 405 1 984 7,26× 9/9
DLinear ETTh1 2 785 8 103,3 14,5 7,11× 3 515 486 7,23× 9/9
DLinear ETTh2 2 785 8 103,3 14,2 7,26× 3 514 477 7,37× 9/9
DLinear ETTm1 11 425 8 423,3 58,6 7,22× 14 426 1 988 7,26× 9/9
DLinear ETTm2 11 425 8 422,1 58,9 7,17× 14 407 1 998 7,21× 9/9
PatchTST ETTh1 2 785 1 13 440,8 5 733,4 2,34× 59 973 25 507 2,35× 9/9
PatchTST ETTh2 2 785 1 13 484,3 5 749,2 2,35× 60 068 25 645 2,34× 9/9
PatchTST ETTh1 2 785 8 1 777,5 765,7 2,32× 60 424 25 955 2,33× 9/9
PatchTST ETTh2 2 785 8 1 785,3 769,3 2,32× 60 677 26 070 2,33× 9/9

DLinear: 7,1–7,3× по времени и 7,2–7,4× по тактам, на одном и на восьми потоках. PatchTST: 2,3× по времени и по тактам. На восьми потоках обе сборки тратят в сумме примерно столько же тактов, сколько на одном. Процесс целиком, включая чтение 26–107 МБ окон и запись прогнозов, быстрее в 5,7–6,0 раза для DLinear на одном потоке, в 3,0–3,1 раза на восьми и в 2,3 раза для PatchTST. Подробности, сырые отсчёты и время всего процесса — в measurements.json.

Запуск

python3 tools/make_windows.py --out data --targets   # скачивает официальные CSV ETT (с проверкой sha256),
                                                     # пишет data/windows_*_test.f32 (и целевые значения)
mkdir -p out
for d in ETTh1 ETTh2 ETTm1 ETTm2; do
  bin/ltsf-dlinear-takt --data $d --windows data/windows_${d}_test.f32 --threads 1 --out out/dlinear_$d.f32
done
for d in ETTh1 ETTh2; do
  bin/ltsf-patchtst-takt --data $d --windows data/windows_${d}_test.f32 --threads 1 --out out/patchtst_$d.f32
done
sha256sum -c expected.sha256                 # окна, целевые значения и прогнозы — как в нашем замере
python3 tools/score.py data/targets_ETTh1_test.f32 out/dlinear_ETTh1.f32

tools/run_bench.sh -m dlinear -n 9 -c 2          # обе сборки, с чередованием, побайтная сверка, ядро 2
tools/run_bench.sh -m dlinear -n 9 -t 8 -c 2-9   # восемь потоков на ядрах 2-9
tools/run_bench.sh -m patchtst -n 9 -c 2

--threads T принимает любое T >= 1; прогнозы от T не зависят. Входные и выходные файлы — сырые little-endian float32 (n x 336 x 7 и n x 96 x 7); tools/make_windows.py нужен только numpy.

Linux x86-64 (glibc). Программы собраны для x86-64-v3 (AVX2, FMA, BMI2: Intel Haswell и новее, AMD Zen и новее); инструкции FMA не используются. PatchTST вызывает erff из системной библиотеки C: на glibc, где erff отличается от glibc 2.39 (Ubuntu 24.04), его прогнозы могут не совпасть с expected.sha256, при этом исходная сборка и сборка TAKT по-прежнему совпадают побайтно.

Эквивалентность

Пересборка оригинала

cd source
RUSTFLAGS="-C target-cpu=x86-64-v3 --remap-path-prefix=$HOME/.rustup=. \
  --remap-path-prefix=$HOME/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f=." \
  cargo +1.96.0 build --release --locked
strip --strip-all -o ltsf-dlinear-original target/release/ltsf-dlinear

С Rust 1.96.0 из rustup (с компонентом rust-src) это на нашей машине воспроизводит bin/ltsf-dlinear-original и bin/ltsf-patchtst-original бит в бит; при другом окружении различаются встроенные пути, но не прогнозы.

Более раннее сравнение с PyTorch (этим пакетом не воспроизводится)

2 октября 2026 г. мы замерили те же модели на той же машине в других условиях: без резервирования стенда (39 логических CPU, общих с другими задачами); обе версии на Rust собраны как разделяемые библиотеки под родной CPU (native) и вызывались внутри процесса; PyTorch 2.14 на CPU, eager и torch.compile (Inductor), батчи по 64 или 512. Для каждой реализации число потоков (1, 8 или 32) и размер батча выбирались по наименьшей задержке на валидационной части, затем замерялось время на тестовой части (медиана из 5).

Модель Набор данных Исходный Rust Сборка TAKT Самый быстрый PyTorch (Inductor) PyTorch / TAKT
DLinear ETTh1 28,7 мс (32 потока) 5,1 мс (32) 18,5 мс (32, батч 512) 3,63×
DLinear ETTh2 30,1 мс (32) 5,3 мс (32) 12,5 мс (32, батч 512) 2,34×
DLinear ETTm1 115,4 мс (32) 19,7 мс (32) 65,1 мс (8, батч 512) 3,31×
DLinear ETTm2 114,9 мс (32) 20,4 мс (32) 49,5 мс (32, батч 512) 2,43×
PatchTST ETTh1 593 мс (32) 236 мс (32) 634 мс (32, батч 64) 2,69×
PatchTST ETTh2 519 мс (32) 234 мс (32) 551 мс (32, батч 64) 2,35×

На одном потоке расклад был другим: PyTorch с Inductor и батчами работал быстрее сборки TAKT (DLinear ETTh1: 37,9 мс против 111,4 мс; PatchTST ETTh1: 2,03 с против 5,78 с). В том прогоне сборка TAKT была быстрее исходной версии на Rust в 5,6–5,9 раза (DLinear) и в 2,2–2,5 раза (PatchTST) на выбранных 32 потоках и в 7,2–7,3 и 2,3 раза соответственно на одном потоке. Все числа того прогона — в measurements.json (earlier_measurement_2026_10_02).

Состав

Данные ETT в пакет не входят (CC BY-ND 4.0, их скачивает tools/make_windows.py). Исходный код сборки TAKT не публикуется: мы поставляем сборки.

Исходный текст: README.ru.md

Telegram