レベルの読み込みと破棄を行うとFMallocUnusedが増加する

お世話になっております。

UE5.6と5.7環境でレベルの読み込みと破棄を行うとFMallocUnusedが増加するという現象を確認しており、こちらの挙動の詳細を調査しております。

調査にはそれぞれのバージョンで空のレベルを使い、こちらで用意したレベルの動的読み込みと破棄を繰り返す機能を実装して検証しています。

検証時にはWin64のDevelopmentビルドのパッケージ化を行って確認しています。

こちらの検証環境では、レベルの破棄後に「OBJ GC」を行った後、LLMを確認するとFMallocUnusedが増加している状態です。

複数のマテリアルへの参照があるレベルと、WorldGridMaterialへの参照しかないレベルを用意し、

それぞれのレベルの読み込みと破棄を繰り返して確認を行った所、どちらもFMallocUnusedが増加しており、

複数のマテリアルへの参照があるレベルはWorldGridMaterialへの参照しかないレベルに比べ、より大きく増加していることを確認しています。(※)

こちらの挙動が意図通りの挙動かどうかご教示いただけたらと考えております。

以上となります。ご確認のほどよろしくお願いします。

※5.7ではレベルの破棄後に、「DistanceFields.DistanceFieldBrickTexture」という項目が残る状態になっていることを確認しており、こちらの変化がFMallocUnusedにどれだけ影響を与えているかは判断できておりません。

[Attachment Removed]

再現手順
添付zipファイル内のreadmeに詳細を記載しています。

サイズ上限の関係上、5.6のプロジェクトファイルはLevelLoadTestProject.zipとして別途添付しており、内容はここに添付している物と同じ物です。

[Attachment Removed]

お世話になっております。

検証しやすい再現プロジェクトをありがとうございます。

結論から申し上げますと、この文脈でのFMallocUnusedの増加は想定の範囲内であり、「ゲームを通じてFMallocUnusedの値が増加し続ける​」「FMallocUnusedの増加の結果Out of Memoryクラッシュが起こる」といった傾向がない限り、それほど問題視する必要はございません。

FMallocUnusedはメモリアロケーターが確保したメモリ領域のうち、未使用の量を示す値ですが、その内訳は

・①​OSにまだ返却していない(未使用の)メモリ領域

・②確保単位のメモリブロックの一部が使用されており、OSに返却不可能な(未使用の)メモリ量

の①と②を足したものとなっています。

②は断片化の指標にはなりますが、ブロック内で空いた部分​のサイズにフィットするメモリ割り当て要求が来た場合に利用されますので、必ずしも「利用不可能状態で放置された死にメモリ」ではありません。

​レベルのアンロードを行いGCを実施しますと、どうしても②の量は増します。そのレベルがより多くのアセットを使用していれば、断片化も比例して増えますので、「複数のマテリアルへの参照があるレベルはWorldGridMaterialへの参照しかないレベルに比べ、より大きく増加する」のも想定の範囲内と言えます。

どうしても気になる場合、①に関しては Engine->TrimMemory(); を呼び出すことで、手動で解放することができます。

お預かりした再現プロジェクトの範囲では、メモリリークの傾向もないため、上記の回答となります。

もし実際のプロジェクトで ​FMallocUnused の一方的な上昇傾向がみられるようでしたら、ご相談ください。

■ご参考​

パッケージ起動時に -llmcsv オプションをつけますと、指定の間隔で llm の統計がCSVの行にダンプされます。

二種類のCSVが吐き出されるうち、「LLM_Pid~.csv」​のほうをチェックすると、FMallocUnusedの時系列変化を追うことができます。

デフォルトでは相当な頻度でCSVへの書き込みを行いますので、-ExecCmds=“LLM.LLMWriteInterval <指定秒数>” を併用して、書き込みペースをご調整ください。

以上、よろしくお願いいたします。​

[Attachment Removed]

ご回答ありがとうございます。

FMallocUnusedの内訳について把握しました。

FMallocUnusedの調査方法についてもご共有ありがとうございます。

今回のサンプルプロジェクトでは軽微な増加で済んでいますが、実際のプロジェクトではプレイを続けているとFMallocUnusedの値が900MB以上まで増加し、最終的にメモリ不足でクラッシュしてしまう状態になっています。

実際のプロジェクトでTrimMemoryも試しましたが効果は無く、「②確保単位のメモリブロックの一部が使用されており、OSに返却不可能な(未使用の)メモリ」が大量に存在している状態であると認識しております。

また、同じレベルの読み込みと破棄を繰り返してもFMallocUnusedの値は増加し続けないので、メモリリークが発生しているわけではないと考えています。

参考にXSSで30分ほどゲームプレイを行った際のLLMCSVを添付させていただきます。

こちらの解決、原因調査のための手段があれば、ご教示いただけますと幸いです。

以上となります、ご確認のほどよろしくお願いします。

[Attachment Removed]

お世話になっております。

BOXにデータをアップロードさせていただきました。

ご確認のほどよろしくお願いします。

ご質問への回答は以下の通りです。

・②に関して

パーシスタントレベルに紐づいている物は下記関数でロード/アンロードを行っています

UGameplayStatics::LoadStreamLevel
 
UGameplayStatics::UnloadStreamLevel

それ以外は下記ですが、基本的には前者が使用されていたと記憶しています。

ULevelStreamingDynamic::LoadLevelInstance
 
ULevelStreaming::SetIsRequestingUnloadAndRemoval

・③に関して

XSXでBinned3を使用しての確認を行おうとしたところ、アプリケーションの起動ができない状態でした。

PS5の方で比較して改善が見られるようであれば、後日改めてデータを共有させていただきます。

以上となります。よろしくお願いいたします。

[Attachment Removed]

お世話になっております。

OOMにつながる​断片化が起きている状況であるとのこと、承知いたしました。

また、LLMCSVのデータもありがとうございます。​

LLMCSVのデータからはただちに「このカテゴリのメモリ用途が怪しい」といったものを見つけることはできませんでした。唯一、UI関係のメモリが増え続けている点が気になりましたが、量的にはわずかなものであり、今回の断片化の主因ではなさそうです。

> こちらの解決、原因調査のための手段があれば、ご教示いただけますと幸いです。

レベル遷移時に寿命の長短が異なる​オブジェクトが同じプールからメモリを取ってしまい、断片化したブロックが累積していっているという状況だと思われますので、このような動向をMemory Insightsで分析いただくのが原因調査のために有効です。

まず、確実にFMallocUnusedが増えていく​特定の2レベル間の遷移を見つけて、

-trace=default,memory,metadata,assetmetadataあたりのトレースオプションを指定して、遷移を繰り返し、怪しいオブジェクトがないかMemory Insightsで​分析していただくことは可能でしょうか?

https://dev.epicgames.com/documentation/ja\-jp/unreal\-engine/memory\-insights\-in\-unreal\-engine

.utraceを送っていただければ、​弊社側で分析を行うことも可能です。

以上、よろしくお願いいたします。​

[Attachment Removed]

お世話になっております。

返答が遅くなり申し訳ございません。

.utraceファイルですがプロジェクト都合で添付できないため、要所での差分のスクショを添付させていただきます。

背景オブジェクトの少ないレベルと多いレベルの2レベル間を複数回移動した際のトレースファイルを確認したところ、FMallocUnusedの増加とともに「Textures」と「Shaders」の項目が増加していることが確認できました。

添付画像1枚目が1往復時点での差分で、2枚目が1往復目と3往復時点での差分です。どちらもオブジェクトが少ないレベル上どうしの比較です。

Texture項目は1往復時点で大きく増加しており、Shaders項目は往復する度に徐々にですが増加していることが確認できました。

FMallocUnusedは1往復目に大きく増加している状態です。

初回ロード後に常駐しているテクスチャの影響かと考え現状有効にしている「r.Streaming.DropMips」を無効にしてみましたがFMallocUnusedの増加量が15MBほど減少したのみでした。

とはいえTexture項目以外で大きく増加しているような項目も無いので、Texture中心に調査を進めようとは考えています。

1往復目以降は微量ですがFMallocUnusedが増加している状態となっており、同時に「Shaders」項目も微量に増加しています。

こちらはリークしているマテリアルが無いか確認します。

トレース中にFMallocUnusedの値は400MB以下にはなっておらず、正常にUnusedが再利用されていない状態であるのではないかと考えています。

.utraceファイルから確認できた情報としては以上となります。

上記に関して他に確認した方が良い箇所、認識に間違いがあればご教示いただけますと幸いです。

以上となります。ご確認のほどよろしくお願いします。

[Image Removed] [Image Removed]

[Attachment Removed]

お世話になっております。

調査状況のご共有ありがとうございます。

調査の方向性はそれで問題ないかと思います。

追加でいくつか確認させてください。

まず、メモリアロケータはどのタイプ(Binned2, Binned3など)のものをお使いでしょうか?

また、レベルを往復する際に(遷移前・遷移後に)コンソールコマンド memreport -full を利用してメモリレポートを取得してみていただけないでしょうか。このレポートの中に、各ビンの断片化の割合や各プールの情報などが表記されており、どのサイズ帯のメモリ割り当てにおいて断片化が進行しているかがつかめます。

例:
Bin   5456 Fragmentation 13 %, Wasted Mem 0.48 MB, Total Allocated Mem 3.58 MB
(中略)
Pool 39   Size   5456   Allocs    11867  Frees    11282  AveAllocSize   5294  EstPadWaste   92KB  UsedVM   4MB  CommittedVM   3MB  HighSlabs    273  CommittedSlabs    229  FullSlabs    162  PartialSlabs      67

残念ながら、memreportからは「何が断片化の犯人か」までは分かりませんので、断片感を起こしているサイズ帯とmemreport、Insightから得られる情報と組み合わせて追跡することになります。

プロジェクトのご都合で貼れないレポートもあると思いますが、Bin、Poolあたりがレポートされているあたりの行は機密性も低いと思いますので、共有いただければ何かお手伝いできることもあるかと思います。

改めまして、現在の手掛かりから考えますと、テクスチャ、マテリアルから追跡されるのは正しいと思います。動的マテリアルインスタンスがレベル遷移をまたいで長寿命のオブジェクトに掴まれてないかなども念のためご確認ください。

また、問題となっている2レベル間の往復に加え、「(ほぼ)完全に空っぽ」のダミーレベルを作成し、あえてそこに遷移することで、

・不要となったアセットが順次きちんと解放されていくのか

・解放されていないとしたら、何がつかんでいるのか

などを調査することも、長寿命オブジェクトとその関係する割り当てを追跡する手がかりを掴むのに有効かと存じます。

以上、引き続きよろしくお願いいたします。

[Attachment Removed]

お世話になっております。

本件解決のために、まずはMemoryInsightsのプロファイルデータをご共有頂ければと思い、先ほどBOXの招待を送らせて頂きました。お手数おかけしますが、本現象発生時のMemory Insightsのプロファイルデータ、およびシンボル情報をBOXにアップロードして頂けますと幸いです。どうぞ、よろしくお願いします。

[Attachment Removed]

お世話になっております。

前回スクリーンショットで共有させていただいたプロファイルデータのシンボルが手元に残っていないので、新しくプロファイルデータを作成してから共有をさせていただきます。

こちら共有ができ次第、改めてご連絡できればと存じます。

以上よろしくお願いいたします。

[Attachment Removed]

お世話になっております。

こちらBOXへのアップロードを行いました。

ご確認のほどよろしくお願いします。

[Attachment Removed]

お世話になっております。

ご返信まで時間を要してしまい申し訳ございません。​

.utraceのほう確認させていただきました。

(.pdbの同梱もありがとうございます。当方ではシンボル解決がうまく機能しなかったのですが、お手元では動作しておりますでしょうか?)

いろいろ解析を進めておりますが、以下の点が気になっております。

[Image Removed]​まず、​「配置物の少ないフィールド」→「配置物の多いフィールド」→「配置物の少ないフィールド」と遷移しているにも関わらず、MaterialInstanceの量がほとんど変動しない(メッシュがドンと下がるところで、ほとんど下がらない)点が気になっております。この動作は実情に沿っておりますでしょうか? Material Instance だけで5万個近くの長寿命の割り当てがあるようでした。​

​ [Image Removed]​シェーダを展開しているところでは、FMallocUnusedが下がっており、適切に再利用されているようでした。したがって、FMallocUnusedの再利用メカニズムが機能していないというよりは、再利用可能なサイズと、ゲームが割り当て要求をかけているサイズが一致していないことが、FMallocUnusedの右肩上がりの上昇の背景で起きていることかと思われます。

この症状が具体的にどのような割り当てによって生じているのか、当方で​ある程度の仮説を立てておきたかったのですが、断片化が主にどのBinサイズで発生しているかによって疑う点が変わってくるため、大変恐縮ですが次の情報を追加で頂戴できますでしょうか?

■追加でいただきたい情報①​

・「配置物の少ないフィールドのロード」が完了し、ロード & GCが落ち着いたタイミングで memreport -full

・同じく「配置物​の多いフィールドのロード」のロード&GCが落ち着いたタイミングで memreport -full

・最後に「配置物の少ないフィールド」に戻ったときの、同条件のタイミングで memreport -full

この3つのレポートをBoxにアップしていただけますでしょうか。​

■追加でいただきたい情報②

​・レベルの破棄とロードで使用している手法

(レベルインスタンスのOn/Off?データレイヤーの切り替え?マップのロード?)

※機密情報が含まれる場合は、EPSの返信に含める形でなく、Box内のテキストにご追記いただければと思います。

■追加でいただきたい情報③

・アロケーターをBinned3に変更した場合に症状がある程度改善されるかどうか

以上、よろしくお願いいたします。

[Attachment Removed]

お世話になっております。

早速のデータのご提供誠にありがとうございました。

​memtrace大変参考になりました。ありがとうございます。また、memtraceをとる際に.utraceも取り直していただいて助かりました。

.utraceへのブックマーク記録より、①過疎マップ→②密マップ→①過疎マップとレベル遷移をする中で、01_Memreportは1m4s付近(①に対応)、02_Memreportが1m52s付近(②)、03が2m30s付近(③)で取得されたという前提でデータをみていっております。

なお、③の地点で Cached free OS pagesが324mb相当あり、これはTrimMemoryでOSに返却できるはずです。

ご報告いただいた話としては、①→②→①→②→①→②…と繰り返すことで FMallocUnused が上昇していくというお話でしたので、最初の1往復①→②→①で異常の兆候が見られないかチェックしました。これには、Unreal InsightsのInvestigationにおいて「Memory Leaks」を使用し、Aマークを①地点、Bマークを②、Cマークを③に置くことで解析をしています。テーブル表の列に「Asset」「Class Name」などを追加することで、ある程度、割り当ての素性も追うことはできました。

MemreportからはスモールメモリのBinに右肩上がりの兆候がややみられましたので、そこを中心に確認をしております。

[Image Removed]この視点でみると、テクスチャなどもリストに出てくるのですが、同一の2地点間の移動ではリークは生まないであろうこと、スモールメモリ帯(一部のBinサイズで右肩上がりの兆候がある)に焦点を当てるべきことを踏まえると、「EngineMisc」タグの割り当てのほうが気になりました。ただ、このタグも広範に使われておりますし、②→③での増分はわずかでしたので、レベルを読み替えるたびに新規のメモリ割り当てがみるみる増えているという状況でもなく、お預かりしたデータの範囲ではなかなか断定が難しいというのが正直なところです。

MallocBinned.EnableCSVStatsを1に設定すると、-llmcsvなどの.csvにBinサイズごとのレポートが追加されますので、「どのサイズが膨らみ続け、メモリ返却困難を生み出しているのか」を、長期的に観測する手段として、一度お試しいただければと思います。そのほか気になった点として、

・​「f009」→「常駐UI」の順ではなく、「常駐UI」→「f009」の順でロードしたほうがよいと思われます(より長寿命のものを先に読む)

・レベル遷移後も遷移前のレベルの割り当てが見つかるようでした(②の時点で「f009」、③の時点で「f008」の名を冠したアセットがMemory Insights上で見つかる)…これが暫定保持されているものか、何らかの理由で残留しているのか、当方では判断ができませんでした。

・同様に、レベル名の一部を冠したUIミニマップテクスチャが長寿命化しているようでした。

といった点が挙げられます。後ろの2項目については要確認と考えておりますが、同一2地点の往復では真相がつかみづらいため、より異なるレベルを次々に移動するゲームプレイのほうが問題の有無をつかみやすいかと思います。

一旦お返事申し上げます。​

以上、よろしくお願いいたします。​

[Attachment Removed]

お世話になっております。

ご確認ありがとうございます。

長寿命化しているアセットについては、現在調査を進めております。

>なお、③の地点で Cached free OS pagesが324mb相当あり、これはTrimMemoryでOSに返却できるはずです。

こちら、同様の操作を行った後に「GEngine->TrimMemory()」を実行してみましたが、Memreport上のProcess Physical MemoryとCached free OS pagesに大きな変化が見られませんでした。

こちらは他に設定が必要なのでしょうか。

参考程度ですが、TrimMemory前後のMemreportを共有させていただきました。

お手数ですがご確認のほどよろしくお願いします。

以上となります。よろしくお願いいたします。

[Attachment Removed]

お世話になっております。

お返事が遅れまして申し訳ありません。

> こちら、同様の操作を行った後に「GEngine->TrimMemory()」を実行してみましたが、Memreport上のProcess Physical MemoryとCached free OS pagesに大きな変化が見られませんでした。

> こちらは他に設定が必要なのでしょうか。

大変申し訳ありません。こちらは当方の認識に誤りがございました。​

コンソールでは、​ UE_USE_VERYLARGEPAGEALLOCATOR (以下 VLPA)が有効になっており、メモリの割り当て・解放が FCachedOSVeryLargePageAllocator を通じて行われます。このVLPA使用時は、TrimMemory()呼び出しによるメモリ返却が行われない実装となっておりました。

VLPA は 2MB 単位のページを確保し、そこから 64KB のブロックを取り出して使用します。ページ内に 64KB ブロックが 1 個でも使用中であれば、ページ全体が OS に返却不可能となります。Binned2 のページ内断片化(64KB ページ内に生存オブジェクトが残るとページが解放不能)と合わせて、二重のフラグメンテーション構造があるといえます。

空きブロックは既存ページ内で再利用されるため、VLPA が際限なく新しいページを消費するわけではありませんが、一時的なメモリ消費スパイクがあり、そこで長寿命の割り当てが複数のページに分散した場合、ピーク時に確保された 2MB ページの多くが断片化を起こしたまま残留する恐れがあります。

一部の割り当てが長寿命化している問題は引き続き調査を行っていただく必要がありますが、さしあたり、XSS における OOM 回避に役立ちそうなオプションがありますのでご案内します。

対策①:GPU Scene のメモリ消費スパイク抑制

XSS で OOM を引き起こす具体的な問題として、GPU Scene の InstancePayloadDataBuffer が 1GB を一括で割り当てるケースが社内で報告されておりました。この対策として、 //UE5/Main CL51092079 にて以下の設定値がデフォルトで採用され、弊社の内製タイトルにも適用されております。

r.GPUScene.UseReservedResources=true

r.GPUScene.InstanceDataTileSizeLog2=12

社内ではこの設定によりOOMのリスクが大きく下がったという評価ですが、​御社でも一度お試しいただければと思います。

対策②:VLPA の無効化(UE_USE_VERYLARGEPAGEALLOCATOR=0)

VLPA を無効化すると、メモリ管理が TCachedOSPageAllocator(上限 64MB のキャッシュ)に切り替わり、TrimMemory() による OS へのメモリ返却が有効になります。

ただし、VLPA は元々パフォーマンス最適化として導入されたもので、社内の事例では、無効化によりゲームスレッド約 2ms、レンダースレッド約 1.6ms、RHI スレッド約 1ms 程度の性能劣化が計測されました。どの程度パフォーマンスに影響するかはプロジェクト次第ですが、十分な検証が必要と思われます。

以上、よろしくお願いいたします。​

[Attachment Removed]

お世話になっております。

対策の共有ありがとうございます。

>対策①:GPU Scene のメモリ消費スパイク抑制

該当CVarを提示していただいた値に設定すると、特定画面でOOMが発生する状態になってしまいました。

変更前ではメモリ上限までに4GBの余裕があったことや、

r.GPUScene.InstanceDataTileSizeLog2を「6」まで下げ、

デバッグメモリを10GB有効にしたXSXでも同様の箇所でOOMが発生したことから、純粋な使用量の増加以外の原因を考えております。

参考程度ですが、発生時のログファイルを共有させていただきました。

手元ではCL51092079のみを取り込んでいるので、他に必要な変更があれば共有をお願い致します。

>対策②:VLPA の無効化(UE_USE_VERYLARGEPAGEALLOCATOR=0)

全部のシーンに影響が出るのは許容できないのでVLPAの無効化は無しという方針で進めることとなりました。

また、以前確認していただいた長寿命化しているMaterial Instanceについて、

該当の状態を引き起こしているであろうプロジェクト側起因のリークを確認したので対応を行っております。

以上となります。よろしくお願いいたします。

[Attachment Removed]

お世話になっております。

設定の試行と対策のご検討をありがとうございます。

>対策①:GPU Scene のメモリ消費スパイク抑制

まず、r.GPUScene.UseReservedResources=trueおよびr.GPUScene.InstanceDataTileSizeLog2=12の設定ではお役に立てなかったということで、お時間を無駄に使わせてしまい申し訳ありませんでした。

こちらは GPU Scene バッファの再確保時に発生するメモリスパイクを抑制するためのものですが、XSS|Xプラットフォームであれば GPU 側のスパイクが減った分 CPU 側のメモリプレッシャーが緩み、弊社内製タイトル同様、OOM 回避につながるのではないかという期待がございました。

しかし、御社のプロジェクトでは逆にこの予約自体が仮想アドレス空間を圧迫し、新たな OOM を引き起こしてしまったようです。大変失礼いたしました。

調査しましたところ、CL51092079以外に取り込みが必要な変更は見つかりませんでしたので、純粋にアプローチが間違っていたということになります。

大変恐縮ですが、CL51092079のバックポートは取り消していただき、もし UE5.8 にアップグレードする計画などあるようでしたら、(今回の問題が再発しますので)その際は CVar でUseReservedResourcesの機能を抑制するなどして対処していただければと思います。

>対策②:VLPA の無効化(UE_USE_VERYLARGEPAGEALLOCATOR=0)

こちらは無効化しないとのことで承知いたしました。

そうしますと、いまお調べいただいている Material Instance のリーク修正で改善が見られれば・・・というところになりますが、

当方でもほかにエンジンレベルで動作を改善する余地がないか、引き続き情報を調べてまいります。

以上、引き続きよろしくお願いいたします。

[Attachment Removed]