GPUを使った分散処理で見落としがちなCPUボトルネックとtasksetによる解決法

GPUを使った分散処理で見落としがちなCPUボトルネックとtasksetによる解決法

こんにちは!

複数枚のGPUをつかった並列処理システムを設計しているときCPUについてはあまり考えないでシステムを設計してしまうことがあります。

「機械学習システムの主役はGPUなんだから、CPUなんて、あんまり気にしなくてよいのでは」

いいえ、そうでもないんです。

推論中のあるタイミングに急に動作が遅くなったりするときCPUが原因であることがけっこうあります。

概要(5分で分かる要点)

先日GPUを使った並列処理システムで、予期しないCPUボトルネックが発生し、パフォーマンスが大幅に低下する問題に遭遇しました。

複数のプロセスが異なるGPUを使用しているにも関わらず、処理が極端に遅くなる現象の原因は、処理パイプラインの一部に含まれるCPU集約的な計算処理でした。

問題の症状

  • 単一プロセス実行時:正常な速度
  • 複数プロセス並列実行時:処理時間が数倍に増加
  • GPUリソースに競合なし(nvidia-smiで確認済み)

根本原因

処理パイプラインにGPUに適さないCPU集約的な計算(データ前処理、統計変換など)が含まれており、複数プロセスが同じCPUコアを奪い合うことで、キャッシュ競合とコンテキストスイッチが頻発していました。

解決方法

tasksetコマンドで各プロセスを特定のCPUコアに固定
→これにより、複数プロセスで同じCPUコアを奪い合うことを回避
 うまいことやってよ!、って思いますが、
 複数のプロセスが CPU の1番目のコアを使おうとしていました
 こういう部分もちゃんと制御してやる必要があるんです!

# プロセス1をCPU 0-7に固定
taskset -c 0-7 python process1.py --gpu 0

# プロセス2をCPU 8-15に固定
taskset -c 8-15 python process2.py --gpu 1

適用のポイント

  • Intel Coreシリーズ:P/Eコアの特性を考慮してCPU集約的タスクはPコアに配置
  • Intel Xeonシリーズ:NUMAアーキテクチャを考慮し、同一ソケット内でコアを割り当て
  • システムプロセス用のコアを確保:すべてのコアを占有せず、1-2コアは空けておく

このごく簡単な設定により、並列実行時のパフォーマンスが劇的に改善し、ほぼ理想的な並列化効率を達成できました。

コロンブスの卵ですね。

さて、ざっくり版はここまでです。
以下、問題原因の特定やメカニズム、CPU特性に応じた設定方法について詳細編につづきます

詳細版:技術的背景と実装方法

さて、概要はあんなかんじですが、
ここからは問題詳細と具体的な実装方法について解説いたします。

問題発生と原因の特定

問題の発見と初期の混乱

ある日、私たちのチームが開発していたマルチGPU処理システムで奇妙な現象が観察されました。
このシステムは、大規模なデータセットに対して並列処理を行うために設計されたもので、複数のGPUを搭載したサーバー上で動作していました。ここではGPU2枚の話にシンプル化してご説明いたします。

単一のプロセスで処理を実行した場合、期待通りの処理速度で動作し、GPUの使用率も適切な水準を維持していました。しかし、2つのプロセスを異なるGPU(CUDA:0とCUDA:1)に割り当てて同時に実行すると、特定の処理ステップにおいて処理時間が極端に増加するという現象が発生しました。具体的には、単独実行時に数秒で完了していた処理が、並列実行時には数十秒、時には分単位の時間を要するようになったのです。

当初、開発チームはGPUリソースの競合を疑いました。
nvidia-smiコマンドを使用してGPUの状態を監視したところ、各プロセスは確実に異なるGPUを使用しており、GPUメモリの使用量も許容範囲内であることが確認されました。

原因の特定プロセス

GPUリソースに問題がないことが判明した後、私たちは処理パイプライン全体の詳細な分析に着手しました。

各処理ステップの実行時間を計測し、ボトルネックとなっている箇所を特定するため、プロファイリングツールを導入しました。

数日間の調査の結果、処理パイプラインの一部にCPU集約的な計算処理が含まれていることが判明しました。

この処理は全体の処理フローの中では全体の処理時間の数パーセント程度だったのですが、その内容を詳しく分析すると、大量の行列演算を含むデータの前処理段階で必要となる統計的な変換処理でした。具体的には、入力データの正規化、特徴量の抽出、そして複雑な数学的変換です。これらの処理は逐次的な性質を持ち、並列化が困難であったため、GPUではなくCPU用に書かれていました。

どうやらCPUで何かが起こってるということで、さらなる問題分析と問題の根本原因をスタディしました。

問題の根本原因 はCPUのリソース競合

問題の根本原因をさぐっていくと、どうやらCPUのリソース競合ということが最終的に発覚しましたので、以降はその解説をしていきたいとおもいます。

CPUアーキテクチャとコア数の理解

問題を深く理解するために、まず使用しているCPUのアーキテクチャとコア構成を確認する必要があります。

Intel Coreシリーズの典型的なコア構成

世代モデル総コア数総スレッド数PコアPコアスレッドEコアEコアスレッド
第12世代Core i9-12900K162481688
第13世代Core i9-13900K24328161616
第14世代Core i9-14900K24328161616

第12世代以降のIntel Coreシリーズでは、高性能なPコア(Performance-core)と省電力なEコア(Efficiency-core)のハイブリッド構成を採用しています。この構成では、CPU集約的なタスクをPコアに割り当てることが重要です。

Intel Xeonシリーズの特徴

シリーズモデルコア数スレッド数備考
Xeon Gold63541836
Xeon Gold63482856
Xeon Platinum83804080
Xeon Platinum8480+56112最大2ソケット対応

Xeonシリーズの大きな特徴は、マルチソケット構成のサポートです。

例えば、Xeon Platinum 8480+は最大2ソケット構成をサポートし、合計112コア224スレッドという強力な計算能力を実現でっきます

問題発生のメカニズム

CPUリソースの競合がどのように発生するのか、そしてなぜそれが深刻なパフォーマンス低下を引き起こすのかを詳しく解説します。

taskset適用前の状態

複数のCPU集約的なプロセスを同時に実行すると、以下のような状態が発生します

CPUリソースの競合状態

時間軸に沿って見ると、実際には以下のような激しいCPUコアの奪い合いが発生しています

結果: キャッシュ競合、コンテキストスイッチ頻発
処理速度が大幅に低下

taskset適用後の状態
tasksetコマンドを使用してCPUアフィニティを設定すると、状態は劇的に改善します

CPUリソースの分離状態

結果: キャッシュ競合なし、コンテキストスイッチ最小化
各プロセスが安定した処理速度を維持

なぜこのような違いが生まれるのか - 動作原理の詳細

Linuxスケジューラの動作原理

Linuxカーネルに実装されているCFS(Completely Fair Scheduler)は、システム全体の効率を最大化するよう設計されています。このスケジューラは、実行可能なすべてのプロセスに対して「公平に」CPU時間を配分しようとします。具体的には、各プロセスの実行時間を追跡し、最も実行時間が少ないプロセスを次に実行するよう選択します。

通常のワークロードでは、この仕組みは非常に効果的です。しかし、CPU集約的な複数のプロセスが同時に実行される場合、スケジューラは各プロセスを利用可能なすべてのCPUコアに動的に割り当て続けます。この「動的な割り当て」が、実は深刻なパフォーマンス問題の原因となります。

キャッシュ階層とその影響

CPUリソースの競合が深刻な問題となる理由を理解するために、CPUのキャッシュ階層について見てみましょう

CPUキャッシュの階層構造

現代のCPUは、メインメモリへのアクセス遅延を隠蔽するために、多層のキャッシュメモリを搭載しています

CPUキャッシュ階層の構造

重要な特性

  • L1/L2キャッシュ: 各CPUコア専用で、そのコアで実行されるプロセスのデータを保持
  • L3キャッシュ: 同一CPUダイ内のすべてのコアで共有(Intel Coreの第12世代以降ではP/Eコアで分離される場合あり、Xeonではソケット単位)
  • アクセス速度の差: L1キャッシュ(4ns)からメインメモリ(100ns)まで、25倍以上の速度差

キャッシュ競合による性能劣化のメカニズム

この階層構造を理解した上で、複数プロセスが同じCPUコアを奪い合う場合に何が起きるかを見てみましょう:

1. キャッシュの汚染と無効化

プロセスがCPUコアを切り替える際の動作を時系列で追うと以下のようになります。

このように、プロセスがコア間を移動すると、キャッシュの局所性が失われ、低速なメインメモリへのアクセスがふえて性能が大幅に低下します

2. キャッシュスラッシングの発生

また、頻繁なプロセス切り替えが発生すると、以下のような悪循環に陥ります

キャッシュスラッシング状態
メモリ帯域幅の競合

さらに、CPU集約的な処理が大量のデータを扱う場合、メモリ帯域幅の競合も発生します。キャッシュミスが増加すると、メインメモリへのアクセスが増加し、メモリ帯域幅も圧迫されます

  • 理論帯域幅: DDR4-3200で約51.2GB/s
  • キャッシュヒット時: メモリ帯域使用率 5%程度
  • キャッシュミス多発時: メモリ帯域使用率 70%以上

複数プロセスが同時にキャッシュミスを起こすと、メモリコントローラでの競合も発生し、さらなる遅延が生じます。

こういった要因が組み合わさることで、単純に2つのプロセスを実行した場合でも、処理時間が2倍以上に増加する現象が発生してしまうことがあります。

ということで、tasksetによるCPUアフィニティの設定は、これら問題をさくっと一発で解決する、シンプルかつ効果的なアプローチとなります。

Xeonシリーズにおける考慮事項

さて、Intel Xeonシリーズを使用する場合、特にマルチソケット構成では、NUMA(Non-Uniform Memory Access)アーキテクチャを意識しないと沼にはまることになります。

NUMAアーキテクチャとは

NUMAは、複数のCPUソケットを搭載したシステムにおけるメモリアクセスの最適化技術です。各CPUソケットは自身専用のメモリコントローラとローカルメモリを持ち、これらの組み合わせを「NUMAノード」と呼びます。

重要な特性として、CPUは自身のNUMAノードに属するローカルメモリへのアクセスは高速(約70-100ns)ですが、他のNUMAノードのメモリ(リモートメモリ)へのアクセスは、QPI(QuickPath Interconnect)やUPI(Ultra Path Interconnect)を経由する必要があるため、1.5~2倍のレイテンシ(約150-200ns)が発生します。

なぜNUMA最適化が重要なのか

NUMA構成を無視してプロセスを配置すると、以下の問題が発生し沼ります。

  1. メモリアクセスの不均衡
    プロセスがSocket 0のCPUで実行されているが、メモリがSocket 1に配置されている場合、すべてのメモリアクセスがUPIリンクを経由することになり、大幅な性能低下を引き起こします。
  2. メモリ帯域幅の競合
    複数のプロセスが異なるソケットから同一のメモリバンクにアクセスすると、UPIリンクの帯域幅(最大約41.6GB/s)がボトルネックとなります。ローカルメモリの帯域幅(DDR4-2933で約140GB/s)と比較すると、大幅に制限されます。
  3. キャッシュコヒーレンシのオーバーヘッド
    異なるソケット間でデータを共有する際、キャッシュの一貫性を保つためのスヌープ処理が発生し、レイテンシがさらに増加します。

2ソケットXeonシステムのNUMA構成

ということで、XEONの場合尾は、プロセスをNUMAノード単位で割り当てることが重要となります

# プロセス1:NUMA Node 0のCPUとメモリを使用
numactl --cpunodebind=0 --membind=0 taskset -c 0-27 python process1.py --gpu 0

# プロセス2:NUMA Node 1のCPUとメモリを使用
numactl --cpunodebind=1 --membind=1 taskset -c 28-55 python process2.py --gpu 1

NUMAノードをまたぐメモリアクセスは、ローカルメモリアクセスと比較して約1.5~2倍のレイテンシが発生します。そのため、各プロセスが使用するCPUコアとメモリを同一のNUMAノード内に配置することで、メモリアクセスのレイテンシを最小化し、パフォーマンスを大幅に向上させることができます。

問題解決へ

さて、いろいろCPUについて調べて、この問題を解決法がみえてきました。
解決法はシンプルで、どのプロセスをどのコアに割り振るか、を制御するという方法です。そのためにtasksetコマンドを活用することができます。

tasksetは、Linuxにおいてプロセスを特定のCPUコアセットに固定(アフィニティの設定)するための強力なツールです。

それでぇあ、それぞれの環境において、どのようにtasksetを使うかみていきましょう。

Intel Coreシリーズでの実装例

Intel Core i9-13900K(8 Pコア + 16 Eコア)を使用する場合の実装例

# Pコアの確認(通常0-15がPコア+HTT)
lscpu --all --extended | grep "Core"

# プロセス1:Pコアの前半を使用
taskset -c 0-7 python heavy_process1.py --gpu 0

# プロセス2:Pコアの後半を使用  
taskset -c 8-15 python heavy_process2.py --gpu 1

# 軽量なバックグラウンドタスクはEコアへ
taskset -c 16-31 python background_task.py

Intel Xeonシリーズでの実装例

Xeon Gold 6348(28コア×2ソケット)を使用する場合

# NUMAノードの確認
numactl --hardware

# プロセス1:Socket 0のコアを使用(NUMA最適化)
numactl --cpunodebind=0 --membind=0 \
  taskset -c 0-27 python process1.py --gpu 0

# プロセス2:Socket 1のコアを使用(NUMA最適化)
numactl --cpunodebind=1 --membind=1 \
  taskset -c 28-55 python process2.py --gpu 1

Windows Subsystem for Linux (WSL)環境での実装

WSL環境でも同様の最適化が可能です。

@echo off
setlocal enabledelayedexpansion

rem システムのCPU情報を取得
for /f "tokens=*" %%a in ('wsl -e lscpu ^| grep "Model name"') do set CPU_MODEL=%%a
echo CPU Model: %CPU_MODEL%

rem CPUコア数を確認(WSL経由)
for /f "tokens=*" %%a in ('wsl -e nproc') do set TOTAL_CORES=%%a
echo Total CPU cores: %TOTAL_CORES%

rem NUMAノード数を確認
for /f "tokens=*" %%a in ('wsl -e lscpu ^| grep "NUMA node(s)" ^| awk "{print $3}"') do set NUMA_NODES=%%a
echo NUMA nodes: %NUMA_NODES%

rem CPUモデルに応じた最適な設定を選択
if "%NUMA_NODES%"=="2" (
    echo Detected dual-socket system, using NUMA optimization
    
    rem Socket 0のコア範囲を計算
    set /a CORES_PER_SOCKET=%TOTAL_CORES%/2
    set /a SOCKET0_END=%CORES_PER_SOCKET%-1
    
    rem Socket 1のコア範囲を計算
    set /a SOCKET1_START=%CORES_PER_SOCKET%
    set /a SOCKET1_END=%TOTAL_CORES%-1
    
    rem NUMA最適化付きでワーカーを起動
    start "Worker 1 (NUMA 0)" wsl -e bash -ic "numactl --cpunodebind=0 --membind=0 taskset -c 0-%SOCKET0_END% python worker.py --gpu 0 --worker-id 1"
    start "Worker 2 (NUMA 1)" wsl -e bash -ic "numactl --cpunodebind=1 --membind=1 taskset -c %SOCKET1_START%-%SOCKET1_END% python worker.py --gpu 1 --worker-id 2"
) else (
    echo Detected single-socket system, using simple core partitioning
    
    rem CPUコアを2つのグループに分割
    set /a HALF_CORES=%TOTAL_CORES%/2
    set /a SECOND_HALF_START=%HALF_CORES%
    set /a SECOND_HALF_END=%TOTAL_CORES%-1
    
    rem シンプルなコア分割でワーカーを起動
    start "Worker 1" wsl -e bash -ic "taskset -c 0-%HALF_CORES% python worker.py --gpu 0 --worker-id 1"
    start "Worker 2" wsl -e bash -ic "taskset -c %SECOND_HALF_START%-%SECOND_HALF_END% python worker.py --gpu 1 --worker-id 2"
)

echo All workers started successfully

CPUアーキテクチャに応じた動的な設定を行うバッチファイルの例

まとめと今後の展望

GPUを活用した分散処理システムの開発において、GPU側の最適化に注目することは当然重要ですが、処理パイプライン全体を俯瞰し、隠れたボトルネックを見つけ出すことも同様に重要だと気づかされました。

特にCPUについては

「GPUとの帯域幅が充分あって、そこそこの性能のCPUのせときゃいいでしょ」

くらいの認識で、あまり深く考えてこなかったので今回の件は本当に学びになりました。

今回の事例のように処理パイプラインの一部に含まれていたCPU集約的な処理が、複数プロセスの並列実行時に深刻なボトルネックとなるシーンは結構あるんじゃないかとおもいます。

tasksetコマンドを使用したCPUアフィニティの設定は、こういう問題に対するシンプルかつ効果的な解決策でした。

特にIntel XeonシリーズのようなマルチソケットシステムでNUMA最適化と組み合わせることで、劇的なパフォーマンス向上を実現できそうです。

一方、Intel Coreシリーズの最新世代をつかうときはPコアとEコアのハイブリッド構成を理解し、適切にタスクを配置することが重要になります。

当社GPUサーバー群は現状基本インテルアーキテクチャを採用してますが、
AMDのチップレット設計、ARMサーバーの普及など、今後は、なにがあるかわかりませんのでこういうCPUの動作原理もちゃんとおさえて今後のものづくりに生かしていきたいとおもいます!

それでは、最後までお読みいただきありがとうございました!
また次回お会いしましょう!

Read more

コーディングエージェントの現状と未来への展望 【第2回】主要ツール比較と構造的課題

コーディングエージェントの現状と未来への展望 【第2回】主要ツール比較と構造的課題

こんにちは! 今回は、コーディングエージェントシリーズ第2回です! 前回の第1回では、2025年12月時点で百花繚乱状態にあるAIコーディングエージェントの全体像を俯瞰しました。 商用サービスからオープンソースまで20以上のツールを紹介し、それらを「CLIベース」「IDE統合型」「AI特化IDE型」「自律型」の4つのカテゴリに分類しました。 また、コーディングエージェントの本質が「LLM+ツール層」のオーケストレーションシステムであること、つまりLLM自体はコード生成と判断のみを担い、実際のファイル保存やコマンド送信はエージェントフレームワーク側が行うという基本アーキテクチャについても解説しました。 さて、今回は、「実際に使い込むと見えてくる課題」にフォーカスします。 正直なところ、どのツールも「すごい!」と感じる瞬間がある一方で、しばらく使っていると「あれ?」と思う場面に遭遇します。 セッションが長くなると急に性能が落ちたり、昨日教えたはずのことを今日は忘れていたり、ベンチマークで高スコアだったはずなのに自社コードではうまくいかなかったり……。 これらは単なる「まだ発

By Qualiteg コンサルティング
LLM学習の現実:GPU選びから学習コストまで徹底解説

LLM学習の現実:GPU選びから学習コストまで徹底解説

こんにちは! なぜOpenAIやAnthropicは世界最高水準のLLMを作れるのに、それに肩を並べる日本発のLLMは存在しないのでしょうか? 技術力の差でしょうか。それとも人材の問題でしょうか。 答えはもっとシンプルです。GPUの枚数とお金です。 今日はそんな 「LLMの学習」にフォーカスをあて、そのリアルについて徹底解説いたします! 1. はじめに 「LLMを自分で学習させてみたい」 そう思ったとき、最初にぶつかる壁がGPUの問題です。 どのGPUを何枚使えばいいのか。クラウドで借りるべきか、オンプレで買うべきか。そもそも個人や小規模チームでLLM学習は現実的なのか。 本記事では、こうした疑問に対して、具体的な数字と事例を交えながら答えていきます。 たとえばLLaMA 2の学習にはA100が2,048枚使われました。DeepSeek-V3は約8億円かかりました。では、あなたの手元のGPUでは何ができるのか。そこを明らかにしていきたいと思います。 対象読者は、LLM学習に興味があるエンジニアや研究者です。PyTorchでモデルを書いたことがある程度の知識を前提とし

By Qualiteg プロダクト開発部, Qualiteg 研究部
今からはじめるClaude Code

今からはじめるClaude Code

こんにちは! 今日は、最近エンジニアの間で話題になっているAIコーディングエージェント「Claude Code」について取り上げます。 AIによるコーディング支援ツールはここ1〜2年で一気に増え、「結局どれを選べばいいのか分からない」と感じている方も多いのではないでしょうか。本記事では、そうした中でClaude Codeを実際に使ってみた所感と、Windows環境での導入・運用の考え方を整理していきます。 AIコーディングツール、どれを使う? 2025年は、AIコーディング支援が一気に“実用品”になり、選択肢が増えすぎて迷いやすい年になりました。 GitHub Copilot、Cursor、Windsurf、Devin、Aider、Cline、OpenHandsなど、商用からオープンソースまで含めると、軽く20種類を超えます。 機能や思想が似ているものも多く、情報を追うだけで疲れてしまう、という方も少なくないと思います。 以前、当社ブログでは「AIコーディングエージェント20選」で全体像を整理しました。 AIコーディングエージェント20選!現状と未来への展望 【第1回】

By Qualiteg プロダクト開発部, Qualiteg コンサルティング
日本語対応 LLMランキング2025 ~ベンチマーク分析レポート~(12月18日版)

日本語対応 LLMランキング2025 ~ベンチマーク分析レポート~(12月18日版)

はじめに 本レポートは、Nejumi Leaderboard 4のベンチマークデータ(2025/12/18版)に基づいて、日本語対応LLMの性能を総合的に分析したものです。 前回は 2025/10/12 版の分析レポートを公開しましたが、たった2か月で劇的な変化がありました! (定期的に最新LLMランキングを更新してまいります。当社のX(旧Twitter)をフォローいただくことで更新情報を受け取り可能です) Nejumi Leaderboard 4は、日本語タスクにおけるLLMの性能を多角的に評価する信頼性の高いベンチマークとして知られています。 本分析では、商用APIモデルとオープンモデルの両方を対象に、それぞれの特徴や傾向を詳しく見ていきます。 オープンソースモデルについて Weightがオープンなモデルは場合によっては「オープンソースモデル」、「OSSモデル」と呼ばれますが、モデルによっては「オープンソース」と呼ぶには不十分な場合があるため本稿では、「オープンソースモデル」ではなく「オープンモデル」と表現しています。 ベンチマーク分析について 本レポートは、

By Qualiteg コンサルティング, Qualiteg プロダクト開発部