☁️

ECS FargateでのEmbeddingがCPUを使い切ってくれない事象の原因と対応

に公開

背景

  • HuggingFaceのモデルをECS Fargateで動かしたら、想定より遅かった(これは結果的に勘違いだった)
  • メトリクスを見たらCPU使用率が50%で止まっており100%使い切れていなかった
  • torch.get_num_threads()を見ると、Fargateだと設定したvCPUの半分の値になっていた

動作環境: ECS Fargate: CPU = 1024 * 16 (16vCPU)
torch.get_num_threads()した結果: 8

この原因と対応を調べる。

原因

torchのスレッド数の初期値はどう決まるのか?

コード的にはこの辺り。

https://github.com/pytorch/pytorch/blob/8dd380803c0e25786cba12801088c420a2ca071b/c10/core/thread_pool.cpp

Claudeさんにコメント入れてもらったもの

  if (cpuinfo_initialize()) {
    // 物理コア数と論理プロセッサ数を取得
    size_t num_cores = cpuinfo_get_cores_count();      // 物理コア数
    num_threads = cpuinfo_get_processors_count();      // 論理プロセッサ数(HTを含む)
    
    // 物理コアが存在し、論理プロセッサより少ない場合は物理コア数を使用
    if (num_cores > 0 && num_cores < num_threads) {
      return num_cores;
    }
    // そうでなければ論理プロセッサ数を使用
    if (num_threads > 0) {
      return num_threads;
    }
  }

要するに物理コア数と論理コア数(スレッド数)の少ない方がデフォルトのスレッド数になるらしい。

物理コアと論理コア(スレッド数)

物理コア: ハードウェア的に存在するコア数。物理的な部品。
論理コア: OSから見えるCPU処理単位の数。

ハイパースレッディングの機能 インテル® ハイパースレッディング・テクノロジーが有効になっている場合、CPU は物理的なコアひとつごとに 2 つの実行コンテキストを露出します。これは、ひとつの物理的コアが 2 つの「ロジカルコア」として働き、異なるソフトウェア・スレッドを処理することができるということを意味します。

ハイパースレッディングとは? - インテル
ハイパースレッディング・テクノロジー - Wikipedia

今回Fargateはx86環境で動かしていたが、x86 FargateのCPUについて調査されていた記事があった。
CPU性能ガチャが激しいという記事だが、Intel XeonのE系やPlatinumが使われているらしい。
AWS ECS Fargate のCPU性能と特徴 | 外道父の匠

実際にECSでCPUを確認する

ECS Execが使えるECSサービスをCDKでサクッと作り、 cat /proc/cpuinfo を実行してみる。
以下は 16vCPUのタスクでの実行結果抜粋

# cat /proc/cpuinfo
processor       : 0
vendor_id       : GenuineIntel
cpu family      : 6
model           : 85
model name      : Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz
stepping        : 7
microcode       : 0x5003707
cpu MHz         : 3098.110
cache size      : 36608 KB
physical id     : 0
siblings        : 16
core id         : 0
cpu cores       : 8
apicid          : 0
initial apicid  : 0
fpu             : yes
fpu_exception   : yes
cpuid level     : 13
wp              : yes
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves ida arat pku ospke
bugs            : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit mmio_stale_data retbleed gds
bogomips        : 4999.99
clflush size    : 64
cache_alignment : 64
address sizes   : 46 bits physical, 48 bits virtual
power management:

解釈

  • Xeon Platinum系列のCPUが使われている
  • cpu cores : 8 - 物理コア数は8
  • siblings : 16 - 論理プロセッサ数は16
  • ht フラグが存在 - ハイパースレッディングが有効

上に書いた通りtorchのスレッド数は物理コア数と論理プロセッサ数の少ない方になるので、この環境では8スレッドになる。

ということで、結論の通りかつ仕様通り、Fargateでtorchのデフォルトスレッド数が決まる理屈とCPU使用率が50%で止まる理屈が理解できた。

対応

torch.set_num_threads()でスレッド数を直接設定することで対応できる。
ただし、vCPU数に合わせてスレッド数を設定するのが最適とは限らない。
スレッド数を増やしても結局は物理的な演算装置は限られており、torch内部の最適化次第ではハイパースレッディングを利用せずに物理コアを使い切れていれば、その方が高速な可能性もある。

性能検証

処理内容

  1. モデルをテキストファイルをS3から取得
  2. 5000件程度のテキストをHuggingFaceのモデルでEmbedding
  3. Embeddingした結果をS3に保存

使用モデル:{glucose link}

※既存の処理を流用したのでtorch以外の部分も多少入ってるが影響軽微なのでそのまま

比較条件

環境:16 vCPU, x64アーキテクチャ, ECS Fargate

計測結果

条件1: 明示的にスレッド数を16に設定

  • 706.89秒
  • 711.16秒
  • 711.82秒
  • 765.91秒

条件2: デフォルトスレッド数のまま(8)

  • 670.46秒
  • 654.07秒
  • 654.07秒

結果

5%~10%ほどデフォルトスレッド数のままの方が早かった...

結論

  • 「Fargateだから」ではなく、ハイパースレッディングが有効で論理プロセッサより物理コアが少ない環境では、torchのデフォルトスレッド数は小さい方の値になる
    • AWSの「vCPU」は論理プロセッサ数と一致する
    • Intelのサーバー向けCPUだと今のところどれもハイパースレッディングが有効
  • torch.set_num_threads(os.cpu_count()) で論理プロセッサ数に合わせて設定可能
    • これによってでCPU使用率は50% → 100%になったが、性能向上を保証するものではない。
    • GLuCoSEでのEmbeddingに関しては、デフォルトのスレッド数(CPU使用率50%)の方が5~10%ほど早かった 😭
  • 教訓:CPU使用率100%なら性能を使い切れているわけではない

Discussion