← ショーケースに戻る

TAKTショーケース:Rustによる時系列予測(ETTでのDLinearとPatchTST)

標準的な長期予測モデルであるDLinear(LTSF-Linear)とPatchTSTを、公式スクリプトでETTデータセットについて学習させ、PyTorchから素のRust(BLASもMLランタイムも不使用)に変換して、2通りにビルドしました。そのRustコードのオリジナルビルドと、同じコードのTAKTビルドです。両ビルドの予測値はビット単位で一致します。両ビルドのビルド済みプログラム、オリジナル版のソースコード、公式データから入力ウィンドウを再生成するスクリプト、ベンチマークスクリプト、当社の測定結果を収録しています。

プログラムの計算内容

ETTデータセットのテスト分割の全ウィンドウを処理します(公式の境界、features M、学習分割でfitしたStandardScaler)。入力は過去336ステップ × 7チャネル、出力は次の96ステップ × 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測定環境)

2026年10月3日に測定。AMD Threadripper PRO 5975WX(Zen 3)、Linux。8コアのCCXを1つ確保し、そのSMTの兄弟スレッドはアイドル状態にしました。シングルスレッドの実行は1コアに、8スレッドの実行はCCXの8コアに固定しています。各データセットについて、ウォームアップを1ペア実行した後、実行順を交互に入れ替えて9ラウンド測定し、中央値をとりました。時間:プログラム自身が計測した予測ループの時間です(ウィンドウの読み込みと予測値の書き出しは除外。どちらのビルドでもコストは同じです)。サイクル数:同じループのユーザーモードのサイクル数です(perf stat、全スレッドの合計)。両ビルドは同じドライバを共有しています。このドライバは長時間稼働するサービスと同様に、解放したヒープメモリをプロセス内に保持し(glibcのmallopt)、ウィンドウごとの一時バッファがページフォールトを起こさないようにしています。測定したのはbin/内のファイルそのもので、tools/run_bench.shを使用しました。

モデル データセット ウィンドウ数 スレッド数 オリジナル(ms) TAKT(ms) 高速化 オリジナル(百万サイクル) 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:シングルスレッドでも8スレッドでも、時間で7.1〜7.3倍、サイクル数で7.2〜7.4倍。PatchTST:時間でもサイクル数でも2.3倍。8スレッドでも、両ビルドの総サイクル数はシングルスレッドの場合とほぼ同じです。26〜107 MBのウィンドウの読み込みと予測値の書き出しを含むプロセス全体では、DLinearがシングルスレッドで5.7〜6.0倍、8スレッドで3.0〜3.1倍、PatchTSTが2.3倍高速です。詳細、生のサンプル、プロセス全体の時間はmeasurements.jsonをご覧ください。

実行方法

python3 tools/make_windows.py --out data --targets   # 公式のETT CSVをダウンロード(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で8スレッド
tools/run_bench.sh -m patchtst -n 9 -c 2

--threads Tには1以上の任意のTを指定でき、予測値はTに依存しません。入出力ファイルは、リトルエンディアンのfloat32の生データです(n × 336 × 7、n × 96 × 7)。tools/make_windows.pyに必要なのはnumpyだけです。

Linux x86-64(glibc)。プログラムはx86-64-v3向けにビルドしています(AVX2、FMA、BMI2:Intel Haswell以降、AMD Zen以降)。FMA命令は使用していません。PatchTSTはシステムのCライブラリのerffを呼び出します。erffの実装がglibc 2.39(Ubuntu 24.04)と異なるglibcでは、予測値が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

rustupのRust 1.96.0(rust-srcコンポーネント付き)を使うと、当社のマシンではbin/ltsf-dlinear-originalとbin/ltsf-patchtst-originalがビット単位で再現されます。別の環境では埋め込まれるパスが異なりますが、予測値は変わりません。

PyTorchとの以前の比較(このパッケージでは再現できません)

2026年10月2日に、同じマシンで同じモデルを別の条件で測定しました。測定環境のリソースは確保せず(39論理CPUを他のジョブと共有)、両Rust版はネイティブCPU向けの共有ライブラリとしてビルドしてプロセス内から呼び出しました。PyTorch 2.14はCPU上でeagerとtorch.compile(Inductor)を使い、バッチサイズは64または512です。各実装について、スレッド数(1、8、32)とバッチサイズは検証分割でレイテンシが最小になるものを選び、そのうえでテスト分割の時間を計測しました(5回の中央値)。

モデル データセット オリジナルのRust TAKTビルド 最速のPyTorch(Inductor) PyTorch / TAKT
DLinear ETTh1 28.7 ms(32スレッド) 5.1 ms(32) 18.5 ms(32、バッチ512) 3.63×
DLinear ETTh2 30.1 ms(32) 5.3 ms(32) 12.5 ms(32、バッチ512) 2.34×
DLinear ETTm1 115.4 ms(32) 19.7 ms(32) 65.1 ms(8、バッチ512) 3.31×
DLinear ETTm2 114.9 ms(32) 20.4 ms(32) 49.5 ms(32、バッチ512) 2.43×
PatchTST ETTh1 593 ms(32) 236 ms(32) 634 ms(32、バッチ64) 2.69×
PatchTST ETTh2 519 ms(32) 234 ms(32) 551 ms(32、バッチ64) 2.35×

シングルスレッドでは順位が逆で、Inductorとバッチ処理を使ったPyTorchの方がTAKTビルドより高速でした(DLinear ETTh1:37.9 ms対111.4 ms、PatchTST ETTh1:2.03秒対5.78秒)。この実行では、TAKTビルドはオリジナルのRustに対し、選択された32スレッドでDLinearが5.6〜5.9倍、PatchTSTが2.2〜2.5倍、シングルスレッドではそれぞれ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.ja.md

Telegram