Hyper-V 上の Linux から GPU を利用する話

どうも、みむらです。

最近は在宅時は NACK5 さんを聞きながら仕事をしていることが増えているのですが、
特に「日常野郎 鬼ガイバー!」と「あまがみ神社」に特にウキウキする今日この頃です。

さてさて。

Hyper-V 上の Linux 上で GPU を使うというのがあります。
主に GPU で機械学習をしたり動画処理したり、私ですと職業柄 hashcat を回してみたり。

色々な記事はあったのですが Linux カーネルを新しめ (執筆時点では 6.8) のもので組むのには、少し手間が掛かってしまったのでここにまとめてみようかと。

なおこの記事は「GPU パーティション分割」機能を用いた構築例となります。
GPU を直接触るものではないため、使い勝手は少し異なりますが、ホスト側と共存して使えるようになるため、個人的にはオススメです。

詳細についてはこのあたりを参照ください:
https://learn.microsoft.com/ja-jp/windows-server/virtualization/hyper-v/gpu-partitioning

今回は、諸事情で Kali Linux (Debian) での紹介となりますが、素のDebian や Ubuntu はもちろん、他のディストリビューションでも転用出来ると思います。


1. ゲスト側の Linux で必要なファイルを WSL2 環境から取り出す

WSL のシステムディストリビューションを起動し、中から必要なファイルを取り出します。

WSL2 はユーザがインストールしたディストリビューション(ユーザディストリビューション)をコンテナとして動かす仕組みになっています。
システムディストリビューションは、それらのコンテナにむけて機能を提供するホストとなっており、WSL が提供する機能のドライバ類はここに入っています。

余談ですが、とくに手を入れていなければ CBL-Mariner が走っています。
https://github.com/microsoft/azurelinux
Azure に馴染みのある方であれば、 “Azure Linux” と呼ばれている軽量な Linux システムとして見たことがあるかたもいらっしゃると思います。

今回ここから取り出すのは、/usr/lib/wsl/lib 以下にあるファイル群になります。


Windows 環境側に作業用フォルダを作り、必要なファイルをコピーして tar を作ります。

なお下記のコマンド例では、作業用フォルダのパスは「 C:\Users\mimura1133\Desktop\work_wsl 」にあるものとします。

方法1:ディスク容量を気にしない場合

この場合はシンプルに
/usr/lib/wsl 以下を tar 固めなどして Windows 環境側に持ち出せばOKです。

ただし、不要なドライバが大量に含まれるため、かなり大きくなります。
(私の環境では 3GB ほどありました)

cd /usr/lib/wsl
cp -R /mnt/(Windows 側作業フォルダまでのパス)/lib.tar.gz lib
cp -R /mnt/(Windows 側作業フォルダまでのパス)/drivers.tar.gz drivers

# コマンド実行例:
# cd /usr/lib/wsl
# bsdtar /mnt/c/Users/mimura1133/Desktop/lib.tar.gz lib
# bsdtar /mnt/c/Users/mimura1133/Desktop/drivers.tar.gz drivers

以上の手順で作成された lib.tar.gz と drivers.tar.gz が後ほど必要になります。

方法2:必要最小限のデータのみを取り出す場合

下記の手順では必要最小限のデータのみを取り出します。
(この方式では、当方環境ですと 1GB 未満になりました)

1. /usr/lib/wsl/lib ディレクトリ以下を取り出す

cd /usr/lib/wsl
cp -R /mnt/(Windows 側作業フォルダまでのパス)/lib.tar.gz lib

# コマンド実行例:
# cd /usr/lib/wsl
# bsdtar zcvf /mnt/c/Users/mimura1133/Desktop/work_wsl/lib.tar.gz lib

2. 現在使用しているグラフィックドライバをコピーする

Windows 側の PowerShell で下記のコマンドを実行して、ドライバを特定します。

Get-CimInstance -ClassName Win32_VideoController -Property * | Format-Table InstalledDisplayDrivers -AutoSize -Wrap

実行例:

上記の例の場合は、 C:\Windows\System32\DriverStore\FileRepository\nv_dispsig.inf_amd64_e6cac7f31a92d62e 以下に必要なファイル群があることが分かります。

上記の結果を用いて、必要なドライバをフォルダごと作業フォルダの “drivers” 以下にコピーします。

# カレントディレクトリ移動
cd C:\Windows\System32\DriverStore\FileRepository
# ドライバをフォルダごとコピー
robocopy /mir (必要なドライバのフォルダ名) (作業フォルダまでのパス)\drivers\(必要なドライバのフォルダ名)
# ini ファイルをコピー
copy (必要なドライバのフォルダ名).ini (作業フォルダまでのパス)\drivers

# コマンド実行例:
# cd C:\Windows\System32\DriverStore\FileRepository
# robocopy /mir nv_dispsig.inf_amd64_e6cac7f31a92d62e C:\Users\mimura1133\Desktop\work_wsl\drivers\nv_dispsig.inf_amd64_e6cac7f31a92d62e
# copy nv_dispsig.inf_amd64_e6cac7f31a92d62e C:\Users\mimura1133\Desktop\work_wsl\drivers

3.ドライバを tar で固める

WSL2 が動く Windows であれば、最初から tar が使えるようになっています。
コマンドプロンプトを用いて下記のコマンドを実行します。

cd (作業フォルダまでのパス)
tar zcvf drivers.tar.gz drivers

実行例:

以上で drivers.tar.gz と lib.tar.gz が出来ました。以降でこれらのファイルが必要になります。


2. Kali Linux をインストールする

通常通りのインストールを実施します。

公式サイトからの VM イメージのダウンロード、iso を用いたインストール、Hyper-V のクイック作成等方法は問いません。

なお、iso からインストールを行う場合は「第2世代」で作成を行ってください。


3. 仮想マシン (VM) の設定を変更する

VMの設定を行います。

既にVM が起動している場合はシャットダウンをおこなった上で
VMの設定画面を開き、いくつか確認を行っていきます。

・セキュアブートが無効になっているか
 ON の場合、後段の dkms 周りで不具合が出る場合があります。

・動的メモリは無効になっているか
 仕組み上動的メモリを有効にすることは出来ないようです。

チェックポイント(スナップショット)は無効になっているか
 これも、本機能有効時には使えないようです。


上記までの設定が終わったら “OK” を押して画面を閉じます。

続けて Powershell を管理者権限で起動し、下記の設定を順に行います。

# VM の情報を取得
$vm = Get-VM -Name "<VM名, ワイルドカードも使えます>"

# GPU の追加
$vm | Add-VMGpuPartitionAdapter

# Memory Mapped I/O 領域の設定
$vm | Set-VM -GuestControlledCacheTypes $true -LowMemoryMappedIoSpace 1GB -HighMemoryMappedIoSpace 32GB

4. VM 内に必要なデータをコピーする

VM を起動し、1つめのステップで作成した lib.tar.gz と drivers.tar.gz を Linux VM 内にコピーします。

※ 2024.06.17 現在、Kali Linux ではこの段階では GUI (Xorg) の起動に失敗します。
後段でGUI 起動が出来るようになりますのでご安心ください。

ファイルのコピーには SCP や curl 等を用いることもできますが、
一番やりやすい方法としては “Copy-VMFile” を用いるのが一番やりやすいかと思います。

管理者権限で起動した PowerShell を用いて下記のように入力します。

# VM の情報を取得。
# (Step 3 で行っている場合は下記コマンドは不要)
$vm = Get-VM -Name "<VM名, ワイルドカードも使えます>"

# ファイルコピー
$vm | Copy-VMFile -FileSource Host -SourcePath .\lib.tar.gz -DestinationPath /
$vm | Copy-VMFile -FileSource Host -SourcePath .\drivers.tar.gz -DestinationPath /

上記手順が完了次第、Linux VM にログインし、
ルート直下に lib.tar.gz と drivers.tar.gz があることを確認します。

(Ctrl + Alt + F1 を押下してログイン画面を表示し、ログインします。)

実行例:


5. コピーした tar.gz ファイルを展開する

コピーしたファイルを VM 内の /usr/lib/wsl 以下に展開します。

# ディレクトリを作成
sudo mkdir /usr/lib/wsl

# lib を展開
sudo tar zxvf lib.tar.gz -C /usr/lib/wsl

# drivers を展開
sudo tar zxvf drivers.tar.gz -C /usr/lib/wsl

# 権限を設定
sudo chmod -R 0555 .

コピー後、下記のようなディレクトリ構造になっていることを確認出来れば OK です。
( nv_dispsig.inf およびそれ以下の部分は、コピーした内容や環境の違いで変わります )


6. コピーしたファイルにパスを通す

下記コマンドを実行しパスを通します。

echo "/usr/lib/wsl/lib" | sudo tee /etc/ld.so.conf.d/ld.wsl.conf
sudo ldconfig

# libcuda.so.1 に関するエラーが出ますが、無視してOKです。

7. カーネルドライバをインストールする

WSL2 に組み込まれているカーネルモードドライバを組み込みます。

この部分のコードは WSL2 のカーネルのソースコードにしかなく、通常、最新のカーネルコードを用いる場合Microsoft 社が公開しているコードでは手入れが必要です。

とても親切なかたがインターネット上にはいらっしゃって、
最新のカーネルで上手く動くようにカスタムして公開してくれています。
https://github.com/Nevuly/WSL2-Linux-Kernel-Rolling

今回はここのコードを用いて、今走っているカーネル向けのドライバを作って入れ込む戦略を採ります。入れ込む部分のコードについても、下記に公開されているものをお借りして進めます。

https://gist.github.com/krzys-h/e2def49966aa42bbd3316dfb794f4d6a


・必要なプログラム・ファイルのインストール

sudo apt install linux-headers-amd64 dkms git

・カーネルモードドライバの作成・インストール

先ほどのサイトからお借りして少し編集したものを下記に示します。
こちらを実行すれば必要なドライバがビルドされてインストールされます。

#!/bin/bash -e

#
# The original version is https://gist.github.com/krzys-h/e2def49966aa42bbd3316dfb794f4d6a
#

if [ "$EUID" -ne 0 ]; then
    echo "Swithing to root..."
    exec sudo $0 "$@"
fi

git clone --depth=1 https://github.com/Nevuly/WSL2-Linux-Kernel-Rolling
cd WSL2-Linux-Kernel-Rolling
VERSION=$(git rev-parse --short HEAD)

cp -r drivers/hv/dxgkrnl /usr/src/dxgkrnl-$VERSION
mkdir -p /usr/src/dxgkrnl-$VERSION/inc/{uapi/misc,linux}
cp include/uapi/misc/d3dkmthk.h /usr/src/dxgkrnl-$VERSION/inc/uapi/misc/d3dkmthk.h
cp include/linux/hyperv.h /usr/src/dxgkrnl-$VERSION/inc/linux/hyperv_dxgkrnl.h
sed -i 's/\$(CONFIG_DXGKRNL)/m/' /usr/src/dxgkrnl-$VERSION/Makefile
sed -i 's#linux/hyperv.h#linux/hyperv_dxgkrnl.h#' /usr/src/dxgkrnl-$VERSION/dxgmodule.c
echo "EXTRA_CFLAGS=-I\$(PWD)/inc" >> /usr/src/dxgkrnl-$VERSION/Makefile

cat > /usr/src/dxgkrnl-$VERSION/dkms.conf <<EOF
PACKAGE_NAME="dxgkrnl"
PACKAGE_VERSION="$VERSION"
BUILT_MODULE_NAME="dxgkrnl"
DEST_MODULE_LOCATION="/kernel/drivers/hv/dxgkrnl/"
AUTOINSTALL="yes"
EOF

dkms add dxgkrnl/$VERSION
dkms build dxgkrnl/$VERSION
dkms install dxgkrnl/$VERSION

また、下記の gist にもアップロードしました。
端末に入れ込む際にダウンロード先として利用頂けたらと思います。

https://gist.github.com/mimura1133/895b2f5f79ca1de1fbd7b0acf10358d6


8. CUDA の環境をインストールする

nvidia-cuda-toolkit のインストールと、ライブラリのリンクの張り替えを行います。

# cuda toolkit のインストール
sudo apt install nvidia-cuda-toolkit

# 不要な追加パッケージの削除
sudo apt remove nvidia-kernel-dkms nvidia-modprobe nvidia-kernel-common

# libcuda.so が cuda toolkit のモノになっているので張り替える。
sudo rm /usr/lib/x86_64-linux-gnu/libcuda.so
sudo rm /usr/lib/x86_64-linux-gnu/libcuda.so.1
sudo ln -s /usr/lib/wsl/lib/libcuda.so /usr/lib/x86_64-linux-gnu/libcuda.so
sudo ln -s /usr/lib/wsl/lib/libcuda.so.1 /usr/lib/x86_64-linux-gnu/libcuda.so.1

ここまで来たら一度再起動を行います。


再起動完了後、nvidia-smi を実行すると下記のような表示となり、
GPU (CUDA) が利用可能な状態になります。

また CUDA を用いるアプリケーションにおいても正常に認識され、 Windows 側のタスクマネージャからも GPU が動作していることが分かります。

CUI で計算を行う範囲で十分であれば、ここで設定は完了となります。
以降は GUI を有効にする場合の手順となります。


(追加) 9. GUI を有効にする

GUI が立ち上がるように設定を入れ込みます。

modesetting ドライバで上手く処理が出来なくなっているのが原因であるため、
BusID とドライバの設定を行い動作するようにします。

内部に露出している PCI の BusID を調べる

一度 xorg を起動して失敗させ、その中のログから調べるのが手っ取り早いです。

# Xorg の起動を試みる。(失敗してエラーが表示されます)
sudo startx

# ログの中から PCI Bus の ID を見つける
grep "PCI" /var/log/Xorg.0.log

# --
# 実行例
# --
grep "PCI" /var/log/Xorg.0.log
[   3.668] (--) PCI:*(0@41715:0:0) 1414:008e:0000:0000 rev 0

上記の実行例の場合、必要になるのはアットマーク以降の “41715:0:0” になります。

調べた BusID を設定に入れる

下記の内容を /etc/X11/xorg.conf.d/hv-fbdev.conf として保存します。

Section "Device"
         Identifier "Card0"
         Driver     "fbdev"
         BusID      "PCI:(上記で見つけた ID)"
EndSection

記入例としては下記のような形になります:

#
# 設定・記入例
#

# sudo vim /etc/X11/xorg.conf.d/hv-fbdev.conf

Section "Device"
         Identifier "Card0"
         Driver     "fbdev"
         BusID      "PCI:41715:0:0"
EndSection

その後、再起動すると GUI が立ち上がってきます。


(追加)10. Kali Linux の Enhanced Mode を有効にする

Enhanced Mode を有効にすることで、クリップボードの共有やスムーズな描画を使えるようにします。

下記ページに従って設定を行った後、追加の設定を行います。
https://www.kali.org/docs/virtualization/install-hyper-v-guest-enhanced-session-mode


kali-tweaks を実行して、初期設定を行う

下記の順番で遷移して設定を行います。
Virtualization → Configure

設定後下記コマンドを入力し、xorg を xrdp を出力先として起動するように設定します。
(先述の 9 で設定した内容と排他になり、Enhanced Mode でのみ GUI が表示されるようになります。)

# Step 9 を実行している場合は、その設定を消去する
sudo rm /etc/X11/xorg.conf.d/hv-fbdev.conf

# xrdp を出力先として起動するように設定する
sudo cp /etc/X11/xrdp/xorg.conf /etc/X11/xorg.conf.d/

設定後 VM をシャットダウンします。

Enhanced Session の接続方式を HVSocket に切替える

管理者権限で起動した PowerShell を用いて、下記のコマンドを実行します。

# VM の情報を取得。
# (Step 3 や 4 で既に実行済みであれば実行不要)
$vm = Get-VM -Name "<VM名, ワイルドカードも使えます>"

$vm | Set-VM -EnhancedSessionTransportType HVSocket

上記が完了したら VM の電源を投入します。

上手く行けば、起動時に下記のようなダイアログが表示されます。

接続を行い、IDとパスワードを入力後、デスクトップが出てくれば完成です。


注意・留意事項等

・マイクロソフト社やその他関連する会社様などから記事の削除・非表示化の指示を受けた場合は予告なく記事を非表示にすることがあります。

・本手法については公式のものではありません。内容に関する質問についてマイクロソフト社やその他の窓口に問い合わせすることはおやめください。また予告なく動作しなくなる場合も考えられます。

・ホスト側のグラフィックドライバの更新などのVM 内のドライバも入れ替える必要がある可能性がある、とのことです(当方環境ではまだ未確認)

執筆に際して参考にした記事など

Hyper-VでGPU(GPU-PV)を利用する方法 (Ubuntu編)
https://qiita.com/Hyper-W/items/5ddfc93891f7b620da8a

Ubuntu 21.04 VM with GPU acceleration under Hyper-V…?
https://gist.github.com/krzys-h/e2def49966aa42bbd3316dfb794f4d6a

GitHub – Nevuly/WSL2-Linux-Kernel-Rolling: Rolling Release Stable Kernel for Windows Subsystem for Linux2 (WSL2)
https://github.com/Nevuly/WSL2-Linux-Kernel-Rolling


検証しながら書いていたら、日が沈んで夜になり、また新たな朝日が昇ってきてしまいました。でもとっても楽しかったのでヨシとします・・!

それではよき GPGPU ライフを VM 内でもお過ごしください!

Hyper-V の第2世代で Linux の起動ログを追いかける

どうも、みむらです。

Hyper-V 上で Linux を動かすかとやっていたのですが
内部で起動がコケてしまって、その追跡にとても時間が掛かったのでそのメモです。

1.COM ポートを生やす

PowerShell で “Set-VMComPort” コマンドを使うことで生やせます。

Set-VMComPort (Hyper-V) | Microsoft Learn
https://learn.microsoft.com/en-us/powershell/module/hyper-v/set-vmcomport

Set-VMComPort -VMName "Gentoo Linux" -Number 1 -Path \.\pipe\vm_debugcom

-VMName は VM 名、 -Number は 1を最小値として ttyS0 に当たっていきます。

2. COM を見る

手っ取り早いのが PuTTY です。
管理者権限で起動した PuTTY で、名前付きパイプの文字列をそのまま入力すると繋いでくれます。

その後、起動オプションに CONSOLE の設定を入れて起動します。
サンプルとしては下記のような感じです:

linux /vmlinuz-6.5.4-gentoo console=ttyS0,9600
boot

3.ログを眺める

コンソールの内容が PuTTY 側に流れてきます。

今回の場合はどうやら “noxsave” を入れれば治るらしいのですが、
気持ち悪いので原因調査をしてみます。


“XSAVE has to be disabled as it is not supported by this module” ということで、
これで検索を掛けて見ると、下記のパッチが見つかります。

https://lwn.net/ml/linux-kernel/1678386957-18016-3-git-send-email-ssengar@linux.microsoft.com/

コードを抜粋するとまさにこんな感じ。

+++ b/arch/x86/hyperv/hv_vtl.c

+static int __init hv_vtl_early_init(void)
+{
+	/*
+	 * `boot_cpu_has` returns the runtime feature support,
+	 * and here is the earliest it can be used.
+	 */
+	if (cpu_feature_enabled(X86_FEATURE_XSAVE))
+		panic("XSAVE has to be disabled as it is not supported by this module.\n"
+			  "Please add 'noxsave' to the kernel command line.\n");
+
+	real_mode_header = &hv_vtl_real_mode_header;
+	apic->wakeup_secondary_cpu_64 = hv_vtl_wakeup_secondary_cpu;
+
+	return 0;
+}

調べてみると、 “Enable Linux to boot in VTL context” という項目を発見。

切ってみたところ、正常に起動しました。


というわけで、第2世代でコンソールを生やして中身を追いかける話・・でした!
エラーメッセージを見れば一撃なんですけど、ここにたどり着くまでに数時間溶かしてしまったので、まだまだ精進します、、、💦

ElasticSearch OSS / OpenSearch の Beats が “runtime/cgo: pthread_create failed: operation not permitted” で落ちるのを直す

どうも、みむらです。

某所の監視に Wazuh ( https://wazuh.com/ )をよく使っているのですが
内部的に ElasticSearch OSS 7.10.2 を使っているため、新しすぎる OS 上で動かそうとすると filebeat が下記のように落ちてしまうことがあります

runtime/cgo: pthread_create failed: Operation not permitted

もちろん、この問題は新しいバージョンの Beats では解決されているのですが
他方で 7.13 以降の Beats は OpenSearch 1.x や Elasticsearch OSS などで使えないという問題があります。

https://opensearch.org/docs/latest/clients/agents-and-ingestion-tools/index/

この問題については、下記のコメントにあるように elasticsearch 側もアップデートすることが推奨されていますので、公式に解決される可能性は低そうです。

https://github.com/elastic/beats/pull/26305#issuecomment-863472649

枯れたものを使え、公式でサポートしていないディストリビューションを使うな、というのは一理あるのですが、その方法で回避するのは面白くないですので、修正含めてやってみました。

(wazuh のサポートで解決している例が見当たらなかったこともあり、最初だけ英語で併記します。)

注意:

修正は自己責任でお願いします。本番用環境に対して独自ビルドを行ったものを適用したことにより問題が発生しても当方では責任を負えません。


落ちる原因 (Cause):

“clone3” のシステムコールが seccomp の許可リストに登録されていないため。
en : The systemcall “clone3” is not allowed by seccomp.

glibc 2.34 以降において、 pthread_create() を呼び出す際に clone3 システムコールが用いられるようになったことが原因となっています。

修正方法 (Solution):

libbeat/common/seccomp 以下の “policy_linux_386.go” と “policy_linux_amd64.go” に “clone3” を追記する
en: Add “clone3” to policy_linux_386.go and policy_linux_amd64.go under libbeat/common/seccomp.

具体的な追加内容については、まさしく当該する patch がありますのでこれに従います。
https://github.com/elastic/beats/commit/82507fda20bee46cee4808d388a0c809dd01ff13

また glibc 2.35 以降では “rseq” システムコールも用いるそうですので
こちらも併せて対応しておくとよいと思います。
en: It is a recommend to also add “rseq” to policy_linux_386.go and policy_linux_amd64.go under libbeat/common/seccomp, due to the syscall is used glibc >= 2.35.

https://github.com/elastic/beats/commit/f02fa32e0a37d6529983e2181b80bf62e4a16b41


実際にやってみる

実際に上記のパッチを当てて問題が解決するか確認してみました。

確認環境 : Fedora 36 x86_64

続きを読む

CentOS から Fedora に直接アップグレードしてみる

どうも、みむらです。

気がついたら 2022 年になっていましたし、オリンピックも終わっていました。
年が進むのは早いものですね。

同じく年が早いと気づかされたものとして、CentOS Linux 8 のサポート切れがあります。

CentOS Stream 8 にアップグレードしましょう、という話もありますが、
“Stream” は RHEL の “Nightly” の立ち位置になっていますし、
今までのRHEL 互換を求めるなら、Alma Linux Rocky Linux に切替えていくのが正しいように見えます。

Fedora/CentOS Stream/CentOS/RHELの関係性 – 赤帽エンジニアリングブログ
https://rheb.hatenablog.com/entry/202007-fedora-distribution

GitHub の CentOS などから Alma Linux に移行するためのツール (Almalinux-deploy):
https://github.com/AlmaLinux/almalinux-deploy

上記の移行ツールの使用について日本語で書かれた記事:
https://www.server-memo.net/memo/centos8_to_almalinux.html

他方、 CentOS Stream 9 からミラーが変わるようで、
執筆時点 (2022/01/17) では日本のサーバがなく寂しい所でもあります。

CentOS Stream 9 Mirror list:
https://admin.fedoraproject.org/mirrormanager/mirrors/CentOS

今実験用に稼働させているサーバのひとつが CentOS なのですが、
実験用ですし CentOS Stream が “Nightly” の立場に変わるのであれば、
更に上流の Fedora にアップグレード出来るのではないか・・と試してみました。


注意:
下記の手法を試したことにより生じた損害や被害などは当方では全く関知しません。
また、公式ではない方法かつ危険が伴う可能性もあるため、バックアップ等を取り、リスクを取ることが出来る環境でのみお試しください。


続きを読む

Jumbo Frame + WinServer 2019 の Hyper-V の通信は妙に遅い。

どうもみむらです。
最近梅雨に入ったのか夏に入ったのか、色々パッとしなくて困る日々が続いております。

先日 Windows Server 2019 (もしくは Hyper-V Server 2019) を Jumbo Frame 環境下で用いると、通信が遅くなるということが分かりましたのでメモがてら。
同じような問題に遭遇した方の一助のとなればありがたいです。


事象:

Windows Server 2019 上の Hyper-V に展開された VM において
RSC (Receive Side Coalescing, Linux では Large Receive Offload で知られている) が有効
かつ VM が Jumbo Frame の環境において、VMから見て受信方向の通信が遅くなる。

注1:Windows Server 2019 では RSC は既定で有効になっています。
注2:MTU が 2850 bytes を上回る ( >2850 ) 場合に速度低下が発生するようです。

原因:

VMSwitch において Reassemble されたパケットが VMに届かなくなる。

回避方法:

RSC を無効にするか MTU を 1500 にする

方法1:ゲスト VM の MTU を 1500 にする。
 例) ip l set mtu 1500 dev eth0

方法2:ホスト側でRSC 機能を VMSwitch 単位で無効にする
 例)Set-VMSwitch (Switch名) -EnableSoftwareRsc $false

方法3:ゲストVM において ethtool 等を用いて “large-receive-offload” を off にする
 Linux) ethtool -K eth0 large-receive-offload off
 Windows) Set-NetAdapterAdvancedProperty “*” -DisplayName “Recv Segment Coalescing” -RegistryValue 0


パケットの気持ちになってみる

最近パケットの気持ちになる、が一部界隈で有名ですので「なってみよう」と思います。

ざっくりと構成は下記の通り。
Hyper-V サーバ内に立てられた VM-01 との通信について Hyper-V サーバの前段のミラー (CAP-01) と VMSwitch のミラー (CAP-02) で通信をキャプチャして挙動を確認しよう、という構成になっています。

なお、各キャプチャは同タイミングでのものではありません。ご了承ください


VM-01 での curl での速度比較

事象を確認するためにまずは curl で適当な通信を発生させて速度を見てみます。

MTU 1500 の場合は Average Speed が 10.7M となっているのに対し
MTU 9000 の場合は Average Speed が 44024 となっています。


MTU 9000 の時の VM-01 のキャプチャ

上記のようになります。

No. 12 において Seq=141, Ack=1409 をサーバに対して返答していますが
その次にやってきたパケット (No.13) は Seq=16897 になっています。
(通常は直前の Ack と同じ番号の Seq が返ってきます)

そのため No.14 において Ack=1409 を再度送信され、
サーバ側からは No.16 において Seq=1409 の返答(再送)が起きてしまっています。


MTU9000 の時の CAP-02 のキャプチャ

上記のようになります。
No. 13 までは先ほどの VM-01 と同じような流れになっています。

ですが No.14 付近から 1474 bytes ではないパケットが流れはじめます。
またそれを境にして、 Ack と Seq の関係が壊れはじめるのも確認が出来ます。

たとえば No.14 は Seq=1409 として 2882 bytes の通信が行われていますが
その後の ACK (No.16) では Ack=1409 として返答が行われていることが分かり、
通信が正常に行われていないことがここから読み取れます。

またこの 1475 bytes でないパケットについては先述の VM-01 のキャプチャにおいては確認出来ず、その後発生した No.25 の Ack に対する No.26 の 1474 bytes の通信が行われて初めて VM-01 側にパケットが到達しているように見受けられます。


MTU9000 の時の CAP-01 のキャプチャ

上記のようになります。

インターネットを介して No.10 ~ No.19 に掛けて勢いよく通信が行われていますが
その後 No.20 において Ack=1 が返され、No.23 において再送が行われています。

この間 Seq は 12673 まで増えており、この No.19 に当該するパケットは先述の CAP-02 においても No.21 として観測できているように見受けられます。


以上の事象、また CAP-02 においてパケットをとり続けると、 reassemble されたパケットが送信された後に再送要求が発生していること、そして RSC を Disable にするとこれらの事象が解決することから RSC (LRO) が原因と判断しました。

また繰り返しではありますが、
Windows Server 2022 (Preview) や Windows 10 (21H1) では発生しないことを確認していますので Windows Server 2019 特有の問題 ( 1809 ベースの Hyper-V 特有? ) と判断しています。


番外:ドライバベースで治せないかやってみる

Linux のドライバは自由に修正したりして実験できますので、
これで何か出来ないかやってみます。

1.そもそも機能を切る:

netvsc の 603 行目に下記のような記述があります
https://github.com/torvalds/linux/blob/master/drivers/net/hyperv/netvsc.c#L603

/* Negotiate NVSP protocol version */
static int negotiate_nvsp_ver(struct hv_device *device,
			      struct netvsc_device *net_device,
			      struct nvsp_message *init_packet,
			      u32 nvsp_ver)
{

/// 省略 ///

	if (nvsp_ver >= NVSP_PROTOCOL_VERSION_61)
		init_packet->msg.v2_msg.send_ndis_config.capability.rsc = 1;

/// 省略 ///

	return ret;
}

この nvsp_ver で Windows Server 2019 とそれ以降の区別を試みましたが
共に NVSP_PROTOCOL_VERSION_61 (0x60001) が返るため、区別は出来ませんでした。

もちろんですが、”init_packet->msg.v2_msg.send_ndis_config.capability.rsc = 0;” とすると RSC の機能が恒久的に無効になります。


2.別パラメータから値を推測する等で値を修正する:

こちらですが、そもそも VMBus 経由での VMQ の割込が来ないため
修正は難しいという形になりました。

Hyper-V のネットワーク通信は下記のようなアーキテクチャになっています。

vmq コンポーネント

引用元 : https://docs.microsoft.com/ja-jp/windows-hardware/drivers/network/vmq-components

親(ホスト)が持つ NetVSP (VMSwitch) に対して VMBus 経由で接続するアーキテクチャになっており、Linux の netvsc ドライバにおいても 1665 行目付近でその接続が行われていることが伺えます。

struct netvsc_device *netvsc_device_add(struct hv_device *device,
				const struct netvsc_device_info *device_info)
{

/// 省略 ///

	/* Enable NAPI handler before init callbacks */
	netif_napi_add(ndev, &net_device->chan_table[0].napi,
		       netvsc_poll, NAPI_POLL_WEIGHT);

	/* Open the channel */
	device->channel->rqstor_size = netvsc_rqstor_size(netvsc_ring_bytes);
	ret = vmbus_open(device->channel, netvsc_ring_bytes,
			 netvsc_ring_bytes,  NULL, 0,
			 netvsc_channel_cb, net_device->chan_table);

	if (ret != 0) {
		netdev_err(ndev, "unable to open channel: %d\n", ret);
		goto cleanup;
	}

	/* Channel is opened */
	netdev_dbg(ndev, "hv_netvsc channel opened successfully\n");

	napi_enable(&net_device->chan_table[0].napi);

	/* Connect with the NetVsp */
	ret = netvsc_connect_vsp(device, net_device, device_info);
	if (ret != 0) {
		netdev_err(ndev,
			"unable to connect to NetVSP - %d\n", ret);
		goto close;
	}

/// 省略 ///

}

https://github.com/torvalds/linux/blob/9d31d2338950293ec19d9b095fbaa9030899dcb4/drivers/net/hyperv/netvsc.c#L1648


試しに netvsc_receive 関数を下記のように編集してみると下記のような出力が得られました。

static int netvsc_receive(struct net_device *ndev,
			  struct netvsc_device *net_device,
			  struct netvsc_channel *nvchan,
			  const struct vmpacket_descriptor *desc)
{
	struct net_device_context *net_device_ctx = netdev_priv(ndev);
	struct vmbus_channel *channel = nvchan->channel;
	const struct vmtransfer_page_packet_header *vmxferpage_packet
		= container_of(desc, const struct vmtransfer_page_packet_header, d);
	const struct nvsp_message *nvsp = hv_pkt_data(desc);
	u32 msglen = hv_pkt_datalen(desc);
	u16 q_idx = channel->offermsg.offer.sub_channel_index;
	char *recv_buf = net_device->recv_buf;
	u32 status = NVSP_STAT_SUCCESS;
	int i;
	int count = 0;

  // 下記行を追記
	netif_info(netdevice_ctx, rx_err, ndev, "BUF-SIZE : %u, SEC-SIZE : %u, RSC-PKTLEN %u\n",
    net_device->recv_buf_size, net_device->recv_section_size,
    nvchan->rsc.pktlen);

	/* Ensure packet is big enough to read header fields */
	if (msglen < sizeof(struct nvsp_message_header)) {
		netif_err(net_device_ctx, rx_err, ndev,
			  "invalid nvsp header, length too small: %u\n",
			  msglen);
		return 0;
	}

/// 以下省略 ///

BUF-SIZE が Window Size にちょっと足したもの、SEC-SIZE が MTU にちょっと足したものの値になり、 RSC-PKTLEN がフレームサイズと同じ値を指し示すようです。

RSC-PKTLEN の値は 1474 を示しており、冒頭のパケットキャプチャと同じような感じになっていることが読み取れます。

なお、同じ VM を同じ設定で Windows 10 の上に構築した場合は下記のようになります。

RSC-PKTLEN が十分に大きな値になっており、 RSC にて reassemble されたパケットが受信出来ていることが分かります。

ドライバを追いかけてみたのですが、正しい値が別パラメータに入っていることなどはなく、また NetVSP の割込が来ないため修正は難しいと考えられました。


まとめ

Windows Server 2019 の Hyper-V を使用して VM を作成する場合は

・MTU を 1500 以下に設定して RSO が正常に機能するようにして使う
・Jumbo Frame を有効にしたい場合は RSO を Disable にする

のどちらかで利用しないと、落とし穴があるという話です。

執筆時点の最新版である “10.0.17763.1999” でもこの事象は発生していますので、
お気をつけくださいませ。


P.S.

割と海外のフォーラムだと “RSO を無効にしたら良くなった!” 的なのはちらほら報告されているみたいですね。。修正されたらいいなとぼんやり思ってます。。

https://social.technet.microsoft.com/Forums/en-US/8aa6a88c-ffc8-4ede-abfc-42e746ff5996/windows-server-2019-hyperv-guest-on-windows-server-2019-hyperv-host?forum=winserverhyperv

https://www.doitfixit.com/blog/2020/01/15/slow-network-speed-with-hyper-v-virtual-machines-on-windows-server-server-2019/