Skip to content

クラウドネイティブ

HAMi解説:GPUは遊んでいるのに請求書は減らない理由

HAMiで推論サービス間でGPUを共有するための実務者ガイド。カード丸ごとのスケジューリングがクラスター請求書で最も高価な項目を浪費する理由と、ソフトウェアvGPUが実際にどのように制限を強制するのかを解説します。

Ivan Porta

創業者 兼 プリンシパルエンジニア

10 分で読了
#hami#gpu-sharing#gpu-virtualization#kubernetes#platform-engineering#finops
HAMi解説:GPUは遊んでいるのに請求書は減らない理由

Dynamic Resource Allocationとそのアルファ版のpartitionable-device・consumable-capacity拡張がリリースされる以前、KubernetesはGPUをカード丸ごとの単位でしかスケジュールできず、デバイスの一部を割り当てるネイティブな手段はありませんでした。たとえば、あるポッドが80 GBのH100のうち4 GBしか必要としない場合でも、そのポッドはライフタイムの間GPU全体を占有します。その結果、共有クラスターでは高価なハードウェアが著しく活用されないままになります。ピークトラフィックに合わせて設計されたチャットボットはオフピーク時間帯に専用カード上で遊んだままになりがちで、複数の推論サービスは使用率が低いにもかかわらず別々のGPUで動作し、監視ダッシュボードはすべてのGPUが割り当て済みと表示する一方で、nvidia-smiのようなツールを見ると実際の使用率はごくわずか、ということも珍しくありません。その結果、クラスターが満杯に見えるために、追加GPUのための新たな予算要求が発生します。DRAはデバイスが宣言した予算のオーバーコミットを防ぐことでこのギャップを狭めますが、ポッドが動き出した後のリソース制限をネイティブに強制するわけではなく、それは引き続きデバイスドライバーの責任のままです。ここにHAMiの価値があります。

HAMi(Heterogeneous AI Computing Virtualization Middleware)は、ポッドがGPUのメモリと演算能力を必要な量だけ正確に要求できるようにし、コンテナ内の強制レイヤーがその制限を守らせることでこのギャップを埋めます。アプリケーションやイメージへの変更は一切不要です。SNOWやNIOのような企業がHAMiを本番環境で運用しており、プロジェクトは2026年7月にCNCF Incubatingに到達しました。

HAMiとは、実際のところ何なのか?

HAMiはKubernetes向けのオープンソースGPU仮想化レイヤーです。ポッドがメモリ量とAIアクセラレーターの演算能力を正確に要求できるようにし、それだけの空き容量を持つデバイスにポッドをスケジュールして、ランタイムでそのスライスに紐付けます。同じモデルはNVIDIA GPU、Cambricon MLU、Hygon DCU、Ascend NPU、Moore Threads、MetaXをはじめとする多くのアクセラレーターにまたがり、それぞれ独自のデバイスプラグインと分離バックエンドを介して実現されます。たとえばNVIDIAでは、HAMiは標準のデバイスプラグインを置き換え、コンテナ内でCUDAドライバーAPIをインターセプトすることでメモリ上限を強制します。他のベンダーは同様のライブラリレベルまたはハードウェアレベルの分離を提供します。

ユーザー体験は意図的にシンプルに保たれています。標準的なデバイスリソース要求に1つ以上の制限を追加し、スケジューラーがそれをスケジューリング時に読み取って、十分な空きメモリと演算リソースを持つカードを見つけます。NVIDIAでは、これまでどおりnvidia.com/gpuを要求し、その横に2つの拡張リソースnvidia.com/gpumemnvidia.com/gpucoresを追加するだけです。

resources:
  limits:
    nvidia.com/gpu: 1       
    nvidia.com/gpumem: 8000 
    nvidia.com/gpucores: 30 

この分離モデルの位置づけは、2つの比較によって明確になります。MIGはハードウェアの境界を引きます。各インスタンスは専用のSMを持ち、L2キャッシュスライス、メモリコントローラー、DRAMチャネルを通る専用の経路を確保します。一方、HAMiの境界は「壁」ではなく「フェンス」です。その制限はコンテナ内のソフトウェアで強制されるため、ワークロードを予算内に確実に収めることはできますが、デバイスレベルの障害を封じ込めることはできません。ハングしたプロセス、ドライバーのリセット、共有カード上のXIDエラーは、依然としてそのカード上のすべてのテナントに影響します。ただし両者は異なるレイヤーで動作するため、競合ではなく補完関係になり得ます。HAMiがMIGを直接制御できる点については後ほど触れます。

一方、time-slicingと比べると、HAMiは安定性の面で大きな改善を提供します。time-slicingは単に1枚のカードをスケジュール可能な複数のレプリカとして公開し、それらのワークロードを演算エンジン上でインターリーブするだけで、レプリカごとのメモリ上限も分離もありません。そのため、1つのワークロードが名目上のシェアを超えて割り当てを行い、他のワークロードを道連れにすることが危険なほど容易に起こります。HAMiは代わりに超過分の割り当てを拒否します。次のログでは、PyTorchがnvidia.com/gpumemで設定された1000 MBの制限に一致する総容量を報告しています。ポッドがそれを超えようとしたとき、アロケーターはOOMエラーでこれを止め、他のvGPUテナントには影響を与えませんでした。

(EngineCore pid=95) ERROR 07-11 21:15:46 [core.py:1231] torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 20.00 MiB. GPU 0 has a total capacity of 1000.00 MiB of which 12.00 MiB is free. Including non-PyTorch memory, this process has 1020.00 MiB memory in use. Of the allocated memory 864.68 MiB is allocated by PyTorch, and 41.32 MiB is reserved by PyTorch but unallocated. If reserved but unallocated memory is large try setting PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True to avoid fragmentation.  See documentation for Memory Management  (https://docs.pytorch.org/docs/stable/notes/cuda.html#optimizing-memory-usage-with-pytorch-cuda-alloc-conf)

共有GPUの要求はHAMiの中をどう流れるか

マニフェストから強制されるスライスに至るまで、要求は下図に示す4つの担い手を経由します。デバイスプラグインが各カードの容量を公開し、mutating webhookがポッドをHAMiのスケジューラーへ振り向け、scheduler extenderがノードとスライスを選択し、コンテナ内ライブラリがランタイムでそのスライスを強制します。

各ノードでは、DaemonSetとして動作するdevice-pluginが、ベンダーの管理ライブラリ(たとえばNVIDIAの場合はnvidia-smiを支えるNVML)を介して30秒ごとにローカルデバイスをポーリングします。その後、このテレメトリデータをhami.io/node-<vendor>-registerアノテーションとしてノードにパッチすることで公開します。

kubectl get nodes gtrekter -o yaml
apiVersion: v1
kind: Node
metadata:
  annotations:
    hami.io/node-handshake: Requesting_2026-07-09 09:03:57
    hami.io/node-nvidia-register: '[{"id":"GPU-a2fd58d2-edbe-4cfd-4dbd-d4197b35431e","count":10,"devmem":6141,"devcore":100,"type":"NVIDIA GeForce RTX 4050 Laptop GPU","mode":"hami-core","health":true,"devicepairscore":{}}]'
    nvidia.com/gpu-driver-upgrade-enabled: "true"
  ...

ポッドが作成されると、APIサーバーはHAMiのmutating webhookを呼び出し、webhookはポッドのマニフェストを、NVIDIA、Ascend、AWS Neuronをはじめとするサポート対象のデバイスドライバーと照合します。各ドライバーはコンテナのresources.limitsを確認し、認識できるリソースキーを探します。たとえばNVIDIAドライバーはnvidia.com/gpunvidia.com/gpumemnvidia.com/gpucoresを探し、Cambricon MLUドライバーはcambricon.com/vmluにマッチします。互換性のある要求が検出されるとすぐに、HAMiはポッドのschedulerNamehami-schedulerに更新し、そのポッドがデフォルトのKubernetesスケジューラーではなくHAMiのデバイス対応スケジューリングロジックで処理されるようにします。

注: HAMiのもとでは、nvidia.com/gpu: 1という要求はもはや「カード丸ごと1枚」ではなく「共有された物理カード1枚」を意味し、メモリとコアの制限が正確なスライスを決定します。ただし、nvidia.com/gpumemを省略したポッドは、設定されたデフォルト(カードのメモリの100%)にフォールバックします。

kubectl logs -n hami-system deploy/hami-scheduler -c vgpu-scheduler-extender
I0710 13:51:38.552275  config.go:111] Initializing NVIDIA device
I0710 13:51:38.552355  config.go:248] All devices initialized successfully
...
I0710 14:02:14.161116  devices.go:647] "Resource requirements collected" pod="default/chat-qwen-64c4bd66f9-8qccs" requests=[{"NVIDIA":{"Nums":1,"Type":"NVIDIA","Memreq":4000,"MemPercentagereq":101,"Coresreq":60}}]

vgpu-scheduler-extenderは、クラスター内のすべての物理GPUについてライブな使用状況マップを構築します。各ノードのhami.io/node-nvidia-registerアノテーションからデバイスメタデータを読み取り、そのノードに既に割り当てられているポッドをループ処理して、消費済みのGPUメモリと演算容量を計算します。

kubectl logs -n hami-system deploy/hami-scheduler -c vgpu-scheduler-extender
...
I0710 14:02:14.161182 node_policy.go:82] node node1 used 0, usedCore 0, usedMem 0,
I0710 14:02:14.161191 gpu_policy.go:104] device GPU-a2fd58d2-edbe-4cfd-4dbd-d4197b35431e user 0, userCore 0, userMem 0,
...
I0710 16:54:54.475656 node_policy.go:82] node node1 used 1, usedCore 60, usedMem 4000,
I0710 16:54:54.475667 gpu_policy.go:104] device GPU-a2fd58d2-edbe-4cfd-4dbd-d4197b35431e user 1, userCore 60, userMem 4000,

その後、スケジューラーは十分な容量を持たないノードを除外し、残ったノードを設定済みのHAMiスケジューリングポリシー(nodeSchedulerPolicygpuSchedulerPolicy。Chartのデフォルトはノードレベルでbinpack、GPUレベルでspread)に基づいてスコアリングします。選択されたノードとGPUスライスは、UUID,Type,memMB,coresレコードのカンマ区切りリストとして、ポッドのアノテーション(hami.io/vgpu-devices-allocated)に記録されます。

$ kubectl describe pod -n default embed-bge-f84696cc9-7587j
Name:         embed-bge-f84696cc9-7587j
Labels:       hami.io/vgpu-node=node1
...
Annotations:  hami.io/vgpu-devices-allocated: GPU-a2fd58d2-edbe-4cfd-4dbd-d4197b35431e,NVIDIA,2000,30:;
              hami.io/vgpu-devices-to-allocate: ;;
              hami.io/bind-phase: success
              hami.io/bind-time: 1783702494
...

ポッドがノードにバインドされると、Kubeletはそのノード上のHAMi device-pluginAllocateを呼び出します。プラグインはポッドのhami.io/vgpu-devices-to-allocateアノテーション(プラグインが消費した時点でクリアされるため、上記のdescribe出力では空になっています)を読み取って要求されたスライスをデコードし、対象の物理デバイスと正確な演算・メモリ制限を特定します。その後、コンテナがその特定のスライス上で動作するために必要なもの、すなわち環境変数、ホストパスマウント(強制ライブラリを含む)、デバイスハンドルの詳細を含むレスポンスを返します。

kubectl logs -n hami-system ds/hami-device-plugin -c device-plugin | grep -i 'Allocate Response'
...
I0710 16:54:54.499747  232069 server.go:719] Allocate Response [envs:{key:"CUDA_DEVICE_MEMORY_LIMIT_0" value:"2000m"} envs:{key:"CUDA_DEVICE_MEMORY_SHARED_CACHE" value:"/usr/local/vgpu/584ed76a-0856-40b3-8113-1ad696daa252.cache"} envs:{key:"CUDA_DEVICE_SM_LIMIT" value:"30"} envs:{key:"LIBCUDA_LOG_LEVEL" value:"1"} envs:{key:"NVIDIA_VISIBLE_DEVICES" value:"GPU-a2fd58d2-edbe-4cfd-4dbd-d4197b35431e"} mounts:{container_path:"/usr/local/vgpu/libvgpu.so" host_path:"/usr/local/vgpu/libvgpu.so.v2.9.0" read_only:true} mounts:{container_path:"/usr/local/vgpu" host_path:"/usr/local/vgpu/containers/1a5bac44-7888-4ea5-9382-3ef49ae1f1ba_tei"} mounts:{container_path:"/tmp/vgpulock" host_path:"/tmp/vgpulock"} mounts:{container_path:"/etc/ld.so.preload" host_path:"/usr/local/vgpu/ld.so.preload" read_only:true}]

Kubeletはコンテナ作成時にこのレスポンスをそのままコンテナに適用します。この時点から、HAMiのプリロードされた制御ライブラリがアプリケーションとベンダードライバーの間に位置し、プロセス内でスライスを強制します。このライブラリはドライバーのメモリ割り当て呼び出しをインターセプトし、各要求をスライスの予算と照合し、ワークロードが上限を超えようとするとOOMエラーを返します。またドライバーの管理クエリ(nvidia-smiなど)もインターセプトして書き換え、報告される総容量が割り当てられた制限を反映し、空き容量がその制限から現在の使用量を差し引いた値を反映するようにします。

kubectl exec -n default chat-qwen-64c4bd66f9-8qccs -c vllm -- env
CUDA_VERSION=13.0.2
NVIDIA_VISIBLE_DEVICES=GPU-a2fd58d2-edbe-4cfd-4dbd-d4197b35431e
CUDA_DEVICE_MEMORY_LIMIT_0=4000m
CUDA_DEVICE_SM_LIMIT=60
CUDA_DEVICE_MEMORY_SHARED_CACHE=/usr/local/vgpu/5e999bc3-2dbf-46b0-ba81-cc479a1c11dd.cache
...
 
kubectl exec -n default chat-qwen-64c4bd66f9-8qccs -c vllm -- cat /etc/ld.so.preload
/usr/local/vgpu/libvgpu.so
 
kubectl exec -n default chat-qwen-64c4bd66f9-8qccs -c vllm -- nvidia-smi --query-gpu=memory.total --format=csv,noheader
4000 MiB
[HAMI-core Msg(272:133498069993280:multiprocess_memory_limit.c:703)]: Cleanup on exit for PID 272
[HAMI-core Msg(272:133498069993280:multiprocess_memory_limit.c:739)]: Exit cleanup complete for PID 272

請求書を動かすスケジューリングポリシー

先に述べたとおり、スケジューラーはまず十分な容量を持たないノードを除外します。残った候補について、HAMiはノードレベルとGPUレベルという2つの異なるスケジューリング階層を用いて最適な配置を決定します。現在、HAMiはそれぞれのレベルで2つの主要な戦略、binpack(タスクをできるだけ密に詰め込む)とspread(タスクをできるだけ広く分散させる)をサポートしています。

metadata:
  annotations:
    hami.io/node-scheduler-policy: "binpack"
    hami.io/gpu-scheduler-policy: "binpack"

どちらのポリシーにも、それぞれ異なる運用面・財務面のトレードオフがあります。

ノードレベルの戦略

ノードレベルでは、binpackポリシーはワークロードをできるだけ少ない台数のマシンに密に詰め込みます。このアプローチによりクラスターオートスケーラーは空のノードを素早くドレインして停止でき、クラウドインフラの請求額を直接削減できます。ただし、この密度はテナントを集中させるため、単一のノードやドライバーの障害の影響範囲がはるかに大きくなり、帯域幅の余裕が減って競合が増えることにもつながります。

逆に、spreadポリシーはワークロードを利用可能なすべてのノードに均等に分散させます。これによりノードごとの競合が下がって予期しないトラフィックスパイクへの余裕が生まれ、障害ドメインを安全に分散できますが、すべてのノードを「ウォーム」な状態に保つことになります。その結果、オートスケーラーはクラスターをスケールダウンできず、アイドル容量に支払い続けることになります。

GPUレベルの戦略

GPUレベルでは、binpackポリシーは1枚の物理カードを完全に埋めてから次のカードにワークロードをスケジュールします。これにより、分割されていないリソースを必要とする大規模モデルのために完全に空きのGPUを温存し、1枚のカードを共有する小さなスライスの数を最大化できます。固有のトレードオフとして、可能な限り密度の高いカードが生まれるため、「ノイジーネイバー」の圧力が最大化され、多数のテナントが単一カードのXID障害にさらされます。

一方、spreadポリシーはワークロードを利用可能なすべてのカードに分散させます。これにより演算とメモリ帯域幅の競合が最小化されるため、レイテンシに敏感な推論ワークロードには優れた選択肢になります。欠点は、利用可能なメモリがクラスター全体で断片化することです。つまり、巨大で連続的なメモリを必要とする後続の大規模ジョブは、クラスター全体の空きメモリの合計が理論上は十分であっても、スケジュールに失敗する可能性があります。

これらは配置時のみのポリシーです。HAMiは新しいポッドが到着した時点でスコアリングするだけで、実行中のワークロードを能動的に移行することはありません。したがって財務的な節約はポッドの入れ替わりに伴って実現します。つまりbinpackは伸縮性の高い推論フリートでは大きな効果を発揮しますが、長期間固定されたポッドではその効果はずっと小さくなります。

HAMiとMIG:補完し合うレイヤー

フェンスと壁は異なるレイヤーで動作し、クラスターごとにどちらか一方を選ぶ必要はありません。HAMiは単一のプールから両方を管理できます。動的MIGを有効にすると、HAMiはnvidia-mig-partedを制御して、到着するジョブに合わせてMIGジオメトリを切り出し・再切り出しし、手作業のnvidia-smi mig操作や事前に確定したスライスレイアウトを不要にします。ポッドはこれまでどおりnvidia.com/gpunvidia.com/gpumemを要求し続けます。デフォルトでは、スケジューラーはその要求をhami-coreスライスとMIGインスタンスのどちらでも満たすことができ、ハードな分離が必要なワークロードはnvidia.com/vgpu-mode: "mig"アノテーションで自分を「壁」側に固定できます。トレードオフは粒度です。MIGの配置は最も近い許容ジオメトリに切り上げられます(A100-40GBでの8 GBの要求は2g.10gbインスタンスに配置されます)。hami-coreであれば、要求したとおりの量を正確にフェンスで囲っていたはずです。

運用の現実

  • HAMiはデバイスプラグインを置き換える。 HAMiのデバイスプラグインと標準のnvidia-device-pluginはどちらもnvidia.com/gpuを所有しようとします。同じノードで両方を動かすとリソース登録を巡って衝突し、実際の枚数と仮想化された数の間に容量のギャップが生じ、GPUの割り当ては後から登録されたほうのプラグインが担うことになって、HAMiの分離が静かにバイパスされます。GPU Operatorを運用している場合は、ドライバー、コンテナツールキット、DCGMのためにOperatorを残し、HAMi管理下のノードではそのデバイスプラグインを無効にしてください。HAMiはgpu=onノードラベルでスコープを絞り、初日からクラスター全体に展開するのではなく、プールごとに展開しましょう。

  • スケジューラーイメージはコントロールプレーンに追従する。 hami-schedulerは標準のkube-schedulerイメージをラップしており、現行のChartはインストール時にAPIサーバーのバージョンからそのタグを解決します。つまりKubernetesをアップグレードしても、次のHelmアップグレードまで古いスケジューラーが静かに動き続けます。scheduler.kubeScheduler.image.tagをKubernetesアップグレードのRunbookに追加してください。さもないと、半年後に痛い目を見ることになります。

  • gpucoresはスロットリングであって予約ではない。 スケジューラーは配置時にシェアを確保しますが、ランタイムではそのシェアは上限であって、下限になることは決してありません。リミッターはカーネル起動時にチェックされるトークンバケットであり、ハードウェアパーティションではありません。ターゲット付近での振動、カードがアイドルな間のシェアを超えたバースト、そしてバーストの最中に隣人が到着した際に制限が再度効き始めるまでの数秒間のオーバーラップが起こるものと想定してください(GPU_CORE_UTILIZATION_POLICY=forceを設定すればリミッターを常時有効に固定できます)。SLOにコミットする前に、ノイジーネイバーがいる状態でp99をベンチマークしましょう。

実務的な提言

GPUフリートがA100/H100クラスで、複数のチームに共有されており、使用率ダッシュボードが財務レビューで気まずい思いをさせるようなものなら、今週にでも1つの推論ノードプールでHAMiをパイロット導入してみてください。インストールは半日で収まります。2つのノードにgpu=onのラベルを付け、helm installを実行し、過剰にプロビジョニングされた推論Deploymentを1つgpumem/gpucoresのスライスに変換し、割り当てとDCGMの使用率を見比べながら1週間動かしてみましょう。検証している計算は単純明快です。H100を8基搭載したノードは、クラウドとリージョンにもよりますがオンデマンドでおおよそ年間29万〜48万ドルかかるため、ビンパッキングでドレインできるノード1台1台が現実のお金として戻ってきます。自社保有のハードウェアでは、H100 1枚あたりおおよそ2万5,000〜3万ドルとして、1,000 GPUのフリートで持続的な使用率が10ポイント上がるごとに、250万〜300万ドルの設備投資が何もしない状態から働く状態に変わることになります。

FAQ

HAMiについて、よくいただく5つのご質問。

HAMiはプロダクションレディですか?

はい。プロジェクトは2026年7月からCNCF Incubatingとなっており、Beike、NIO、SNOW Corpのような企業がすでに本番環境で使用しています。

HAMiを使うにはアプリケーションやイメージの変更が必要ですか?

いいえ。プリロードされたライブラリがコンテナ内でCUDA呼び出しをインターセプトします。ポッドはnvidia.com/gpuの横にnvidia.com/gpumemとnvidia.com/gpucoresの制限を追加するだけです。

HAMiの分離はMIGと同じくらい強力ですか?

デフォルトでは違います。HAMi-coreはソフトウェアで強制するため、カードの障害は共有されます。信頼できないテナントにはMIGを使いましょう。ハードな分離のために、HAMi自身がMIGを制御できます。

HAMi対vLLM:どちらもGPUメモリを管理するのでは?

レイヤーが異なります。HAMiは1枚のカードをポッド間で分割し、vLLMは1つのポッド内でメモリを配分します。両者は組み合わせられます。vLLMはHAMiのスライスを自分のGPU全体として認識します。

KubernetesのDRAでHAMiは不要になりませんか?

いいえ。DRAはポッドがデバイスを要求する方法を標準化するもので、コンテナ内で制限を強制するわけではありません。HAMiはDRAドライバーを提供しつつ、HAMi-coreによる強制を続けます。

このサイトは分析のためにCookieを使用しています。