Jasper Lake搭載NUCで作るQSVを用いた省エネエンコードマシン

# はじめに
この記事ではJasper Lake搭載NUCを使ってQSVを用いた省エネエンコードサーバを構築したりする流れをハードからソフト面までふわっと解説していきたいと思っています。
# Jasper Lakeについて
今回用いるCPUはJasper Lake（以下JSLと表記）というプラットフォームのものです。JSLはAtom系列のCPUでミニPCやタブレット、デジタルサイネージや教育用PC向けなどを想定し低電力で安価なCPUです。そしてGPUとしてGen11(第10世代Coreプロセッサ(Ice Lake)と同じ)を搭載していることが特徴に挙げられます。CPU性能に合わせGPU性能も設定されているのですがJSL最上位モデルのPentium Silver N6005ではIntel Core UHD Graphicsが搭載されており、ベース周波数450MHz/バースト周波数900MHz、実行ユニット(EU)が32となっています。これは前世代のグラフィックスと比べ78%の性能向上を果たしているとのことでTDP10W程度ながらかなり期待のできる構成となっています。
# NUCの選定
NUCというとIntelの製品のみになってしまうのですが実際にはミニPC全般でJSL搭載で最上位のN6005でメモリが2枚搭載(後述)できて国内で入手性が良いものという条件で探すとIntel NUC 11 Essential Kit NUC11ATKPEしかなかったという感じ。
# NUCの組み立て

NUCにはメモリとストレージが付属していないので別で用意する必要があります。このNUCはDDR4 SO-DIMMが2スロット、サポート最大がDDR4-2933なので少なくとも2933は出るメモリを、2枚セットで購入。また、ストレージは1個のM.2スロット( Key M、Type 2242/2280 SSD対応、SATA(6Gb/s)もしくはPCIe Gen3×2(16Gb/s)いずれかの接続をサポート)となっているので適当な容量のものを購入しましょう。私が購入したのは下記のパーツです。
* ADATA AD4S320016G22-DTGN [SODIMM DDR4 PC4-25600 16GB 2枚組]
* WESTERN DIGITAL WD_Black SN770 NVMe WDS100T3X0E 1TB PCIe Gen4x4

今回は初検証ということもあってオーバースペックなパーツ構成になっています。ただ、後述するようにメモリはとても重要なのでメモリ速度と2枚挿しでデュアルチャネル構成にする点はおさえておきたいポイントです。ストレージは録画やエンコード用途なのでもう少し遅くても良いので大容量のものをチョイスするのも良さそうです。
![IMG_2840.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1533066/a2b3619d-557d-44ab-72df-b57971637fdd.jpeg)

# OSのセットアップ
今回はUbuntu 22.04を使用しました。USBメモリに書き込んでNUCに挿してあとはインストーラーに従ってインストールを行います。今回はパーティション等も触らずデフォルトで全部使っています。
実はUbuntu 22.04ではデフォルトのLinuxカーネルが5.15.0です。そしてJSLでHWエンコードするにはHuCの有効化が必要であり（HuCを有効化しないと一部のHWエンコードしか使えない）、HuCの有効化とカーネルのアップデートを行います。
まずHuc及びGuCの有効化前の表示を確認。
```shell
$ sudo cat /sys/kernel/debug/dri/0/gt/uc/guc_info
GuC disabled

GuC log relay not created
$ sudo cat /sys/kernel/debug/dri/0/gt/uc/huc_info
HuC disabled
```
無効状態であることがわかります。
以下のファイルを作成しmodprobeにてHuC及びGuCの設定を書き換えます。
```/etc/modprobe.d/i915.conf
options i915.enable_guc=2
```
現在のカーネルと利用可能なカーネルを確認しインストール。
```shell
$ uname -srvmpio
Linux 5.15.0-48-generic #54-Ubuntu SMP Fri Aug 26 13:26:29 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
$ sudo apt search ^linux-image.*?5\.17
(利用可能なパッケージが表示される)
$ sudo apt install linux-image-5.17.0-1017-oem #今回はこのバージョンを利用
$ sudo reboot
```
再起動後、HuC及びGuCの有効化確認
```shell
$ sudo cat /sys/kernel/debug/dri/0/gt/uc/guc_info
GuC firmware: i915/ehl_guc_62.0.0.bin
        status: AVAILABLE
        version: wanted 62.0, found 62.0
        uCode: 327104 bytes
        RSA: 256 bytes

GuC status 0x00000001:
        Bootrom status = 0x0
        uKernel status = 0x0
        MIA Core status = 0x0

Scratch registers:
         0:     0x0
         1:     0x0
         2:     0x0
         3:     0x0
         4:     0x0
         5:     0x0
         6:     0x0
         7:     0x0
         8:     0x0
         9:     0x0
        10:     0x0
        11:     0x0
        12:     0x0
        13:     0x0
        14:     0x0
        15:     0x0

GuC log relay not created
$ sudo cat /sys/kernel/debug/dri/0/gt/uc/huc_info
HuC firmware: i915/ehl_huc_9.0.0.bin
        status: AVAILABLE
        version: wanted 9.0, found 9.0
        uCode: 498496 bytes
        RSA: 256 bytes
HuC status: 0x0007c002
```
確かに有効化が確認できます。
この状態で正常にドライバが読み込めているかをvainfoにてチェックします。
```shell
$ uname -srvmpio
Linux 5.17.0-1017-oem #18-Ubuntu SMP PREEMPT Fri Sep 9 08:27:54 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
(↑Linuxカーネルが5.15から5.17に上がった)
$ sudo apt install vainfo
$ vainfo
error: XDG_RUNTIME_DIR not set in the environment.
error: can't connect to X server!
libva info: VA-API version 1.14.0
libva info: Trying to open /usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so
libva info: Found init function __vaDriverInit_1_14
libva info: va_openDriver() returns 0
vainfo: VA-API version: 1.14 (libva 2.12.0)
vainfo: Driver version: Intel iHD driver for Intel(R) Gen Graphics - 22.3.1 ()
vainfo: Supported profile and entrypoints
      VAProfileNone                   : VAEntrypointVideoProc
      VAProfileNone                   : VAEntrypointStats
      VAProfileMPEG2Simple            : VAEntrypointVLD
      VAProfileMPEG2Main              : VAEntrypointVLD
      VAProfileH264Main               : VAEntrypointVLD
      VAProfileH264Main               : VAEntrypointEncSliceLP
      VAProfileH264High               : VAEntrypointVLD
      VAProfileH264High               : VAEntrypointEncSliceLP
      VAProfileJPEGBaseline           : VAEntrypointVLD
      VAProfileJPEGBaseline           : VAEntrypointEncPicture
      VAProfileH264ConstrainedBaseline: VAEntrypointVLD
      VAProfileH264ConstrainedBaseline: VAEntrypointEncSliceLP
      VAProfileVP8Version0_3          : VAEntrypointVLD
      VAProfileHEVCMain               : VAEntrypointVLD
      VAProfileHEVCMain               : VAEntrypointEncSliceLP
      VAProfileHEVCMain10             : VAEntrypointVLD
      VAProfileHEVCMain10             : VAEntrypointEncSliceLP
      VAProfileVP9Profile0            : VAEntrypointVLD
      VAProfileVP9Profile0            : VAEntrypointEncSliceLP
      VAProfileVP9Profile1            : VAEntrypointVLD
      VAProfileVP9Profile1            : VAEntrypointEncSliceLP
      VAProfileVP9Profile2            : VAEntrypointVLD
      VAProfileVP9Profile2            : VAEntrypointEncSliceLP
      VAProfileVP9Profile3            : VAEntrypointVLD
      VAProfileVP9Profile3            : VAEntrypointEncSliceLP
      VAProfileHEVCMain422_10         : VAEntrypointVLD
      VAProfileHEVCMain444            : VAEntrypointVLD
      VAProfileHEVCMain444            : VAEntrypointEncSliceLP
      VAProfileHEVCMain444_10         : VAEntrypointVLD
      VAProfileHEVCMain444_10         : VAEntrypointEncSliceLP
```
多少エラーは出ていますがSupported profileがズラッと表示されていれば問題なさそうです。
# FFmpegのビルド
今回はQSV対応のFFmpegビルドを行います。ただ、筆者自身があまり良くわかっていない部分があったり環境との兼ね合いで判断が分かれる場面などもあるので完成形と言うようなものではないですが、なるべく要点は抑えるような形で部分的なコマンド例を示しておきます。今回の目的上録画したMPEG2-TSファイルをソースとして、ビデオはQSVのH264、オーディオをlibfdk_aacでAACへエンコードする最低限のオプションで、ライブラリをスタティックにするかしないかとかはお持ちの環境に合わせて調整してください。今回はDocker内で行います。
```Dockerfile
FROM ubuntu:latest AS ffmpeg-build
ENV DEBIAN_FRONTEND=noninteractive
RUN mkdir /opt/.ccache
ENV DEV="make gcc git g++ automake curl wget autoconf build-essential libass-dev libfreetype6-dev libsdl1.2-dev libtheora-dev libtool libva-dev libvdpau-dev libvorbis-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev pkg-config texinfo zlib1g-dev"
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN sed -i -e's/ main/ main contrib non-free/g' /etc/apt/sources.list && \
    apt-get update && \
    apt-get -y install $DEV && \
    apt-get -y install yasm libx264-dev libmp3lame-dev libopus-dev libvpx-dev libaribb24-dev libx265-dev libnuma-dev libfdk-aac-dev i965-va-driver-shaders && \
    apt-get -y install libva-dev libmfx-dev intel-media-va-driver-non-free \
    libass-dev libfreetype6-dev libsdl1.2-dev libvorbis-dev libtheora-dev

SHELL ["/bin/bash", "-c"]

# FFMpeg version
ENV FFMPEG_VERSION=5.1.1

# ffmpeg build
RUN mkdir /tmp/ffmpeg_sources && \
    cd /tmp/ffmpeg_sources && \
    curl -fsSL http://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 | tar -xj --strip-components=1 && \
    ./configure \
      --prefix=/ffmpeg \
      --disable-shared \
      --pkg-config-flags=--static \
      --enable-gpl \
      --enable-libass \
      --enable-libfreetype \
      --enable-libmp3lame \
      --enable-libopus \
      --enable-libtheora \
      --enable-libvorbis \
      --enable-libvpx \
      --enable-libx264 \
      --enable-libx265 \
      --enable-version3 \
      --enable-libaribb24 \
      --enable-libfdk-aac \
      --enable-nonfree \
      --disable-debug \
      --disable-doc \
      --enable-libmfx \
      --enable-vaapi && \
    make -j$(nproc) && \
    make install
```
Dockerの導入
```shell
sudo apt install docker docker-compose
sudo usermod -aG docker $USER
```
適当なディレクトリを作成してエンコード用のテストTSファイルを置いてコンテナをビルドします。
```shell
~/dev$ ls
Dockerfile  test.ts
~/dev$ docker build -t ffmpeg_test .
```
これでビルドは完成なので無事ffmpeg_testというイメージが作成できました。
# エンコードをやってみる
コンテナを立ち上げエンコードしてみます。そもそも/dev/driを渡さないとHWエンコードできないので注意！
```shell
~/dev$ docker run -v ~/dev:/test --device=/dev/dri:/dev/dri -it ffmpeg_test /bin/bash
root@c5519f35d6dd:/# cd test
root@c5519f35d6dd:/test# /ffmpeg/bin/ffmpeg -hwaccel qsv -hwaccel_output_format qsv -dual_mono_mode main -c:v mpeg2_qsv -i test.ts -sn -threads 0 -c:a libfdk_aac -ar 48000 -b:a 192k -ac 2 -vf deinterlace_qsv,scale_qsv=-1:720 -c:v h264_qsv -b:v 3000k -preset veryfast -y -f mpegts out.mp4
(中略)
    Last message repeated 2 times
frame=12751 fps=315 q=33.0 Lsize=   87373kB time=00:03:33.54 bitrate=3351.8kbits/s speed=5.27x    
video:78130kB audio:4995kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 5.109465%
```
エンコード結果はこのようになりました。テスト用の動画は1440x1080の解像度があったため、変換処理としてはMPEG2からのQSVデコード、QSVデインターレース、QSVリサイズ、H264へのQSVエンコード(3Mbps)を行っています。再生時間は3分33秒のテスト動画ですが元TSが410MBだったのに対しエンコード後は85MBとなりました。ちなみにこのNUCのアイドル消費電力は5W程度なのですがこのエンコード中でも14～15W程度でした。ホスト側からintel_gpu_topを使いながらGPUの使用率を見ていたところどれも100%まで使えているようには見えなかったのでGPUにはまだ余裕がありそうな気がしますね。
![スクリーンショット 2022-09-24 192227_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1533066/db2a1db7-bd59-4d36-636d-5afbaf74cdf2.png)
ちなみにintel_gpu_topはaptでintel-gpu-toolsとしてすぐ導入できます。

今回は1本の動画をどれくらいの速度で処理できるかを見ましたが次は配信時などに重要なリアルタイムエンコードが何並列くらい処理ができるかを見ていきたいと思います。
# 並列でエンコードをやってみる
下記のようなシェルスクリプトを作成し同時視聴を想定した負荷をかけてみます。エンコードオプションの違いは-reが入り1倍速処理でFFmpegが動いている点のみです。
```shell:parallel.sh
#!/bin/bash
for i in $(seq 1 $1)
do
  cp test.ts test_$i.ts
done
for i in $(seq 1 $i)
do
  /ffmpeg/bin/ffmpeg -re -hwaccel qsv -hwaccel_output_format qsv -dual_mono_mode main \
  -c:v mpeg2_qsv -i test_$i.ts -sn -threads 0 -c:a libfdk_aac -ar 48000 -b:a 192k -ac 2 \
  -vf deinterlace_qsv,scale_qsv=-1:720 -c:v h264_qsv -b:v 3000k -preset veryfast -y -f mpegts out_$i.mp4 &
done
wait
for i in $(seq 1 $1)
do 
 rm test_$i.ts out_$i.mp4
done
```
8並列でやってみます。
```shell
root@c5519f35d6dd:/test# ./parallel.sh 8
```
実行結果はこんな感じでGPUのVideo Engineが96～100%くらい、ほぼ処理速度1倍から落ちることなく8並列処理が可能でした。
ちなみに消費電力は15～18W程度でした。
![スクリーンショット 2022-09-24 202323.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1533066/f0ac96aa-6b7b-bfeb-8ade-b7740db69a6b.png)
実際にはここまでリソースぎりぎりで利用することは現実的ではないと思いますが、かなりのパフォーマンスです。
# 余談
今まではDDR4-2933 16GB 2枚の合計32GB、デュアルチャネルの構成でした。QSVではメモリの速度が重要という話を小耳に挟んだのでメモリを2枚から1枚に減らしてどのような影響があるか見てみたところ、おおよそですがリアルタイム処理の最大並列数が8本だったところが6本程度が限界になりました。おそらくメモリ容量が32GBから16GBになった影響よりデュアルチャネルからシングルチャネルになった影響だと考察しています。今回のように単なるエンコードマシンに32GBのメモリは過剰なので例えば4GB2枚の8GB程度でも良いのでDDR4-2933以上の速度のメモリを2枚でデュアルチャネル駆動できるような構成がおすすめです。NUC選定時にメモリが2枚挿せるかどうかを考慮していたのもこの点からです。折角省エネコンパクトなシステムでQSVがつよつよなのが良いところなのにメモリをミスると数割パフォーマンスが落ちるという罠があるのです。
また、新世代のIntel Graphicsがすごいから使いたいのにLinuxカーネルが安定な現行版(5.15)だと実行ができません。（ビットレートを指定できないとかかなり制約の下で動かなくはないけど面倒なので詳しくは検証していません。）今回は5.17のパッケージがあったのでアップデートを行い事なきを得ましたが、正直あまり理解せずにLinuxカーネルをアップデートするのは手に負えない悪影響が出る可能性も否定はできないので今回のように独立して割り切った環境でないとちょっとやりづらいですよね。ディストリ公式で安定したバージョンとして5.17以降のカーネルが組み込まれると大手を振って利用していこうという機運が生まれるんですが…。
今回はエンコードサーバとして記事にまとめましたが余力があればチューナーを追加して録画サーバとして稼働させる編もやりたいなと思っています。



# 参考URL
https://newsroom.intel.com/wp-content/uploads/sites/11/2021/01/intel-pentium-silver-celeron-processors-product-brief.pdf

https://emby.media/community/index.php?/topic/107064-quicksync-works-once-per-boot-then-stops-working-and-uses-software/page/2/

https://01.org/linuxgraphics/downloads/firmware

https://qiita.com/Daigorian/items/3b5bf97f58b071e0945d