はじめに
前回の記事で、RamaLamaのコンテナイメージ自動選択の仕組みを調べました。その中で、--backend rocmを指定しても--dryrunの出力に反映されないことに気づいたので、原因をソースコードから追ってみました。
環境
- OS: Ubuntu 24.04
- GPU: AMD Radeon 890M(iGPU)
- RamaLama: 0.24.0
- Podman: 4.9.3
再現手順
デフォルト(Vulkan)
$ ramalama --dryrun run gpt-oss
podman run ... quay.io/ramalama/ramalama:0.24 llama-server ...
AMD GPUではデフォルトでVulkanバックエンドが選択され、ベースイメージramalama:0.24が使われます。
--backend rocmを指定
$ ramalama --dryrun run gpt-oss --backend rocm
podman run ... quay.io/ramalama/ramalama:0.24 llama-server ...
--backend rocmを指定したのに、イメージはramalama:0.24のまま。rocm:0.24にならない。
--backend rocm + --imageを明示指定
$ ramalama --dryrun run gpt-oss --backend rocm --image quay.io/ramalama/rocm:0.24
podman run ... quay.io/ramalama/rocm:0.24 llama-server ...
--imageで明示すればROCmイメージになるが、--backendだけでは切り替わらない。
期待される動作
--backend rocmを指定したら、--dryrunの出力でもイメージがquay.io/ramalama/rocm:0.24になるべき。
原因
args.imageの値が決まるタイミングが2段階あり、--dryrunでは2段階目がスキップされることが原因です。
第1段階: argparseのデフォルト値(--dryrun/通常run共通)
@lru_cache(maxsize=1)
def default_image() -> str:
return accel_image(ActiveConfig())
parser.add_argument(
"--image",
default=default_image(),
help="OCI container image to run with the specified AI model",
action=OverrideDefaultAction,
completer=local_images,
)
--imageのデフォルト値はdefault_image()で決まります。この関数はaccel_image()を呼び、その中でget_container_image(config, gpu_type)が実行されます。
ここがポイントで、argparseのデフォルト値は引数解析前に評価されるため、この時点では--backend rocmがconfigに反映されていません。get_container_image()内のbackendは"auto"のままです。
def get_container_image(self, config: Any, detected_gpu_type: str) -> Optional[str]:
rt_config = self.get_runtime_config(config)
backend = rt_config.backend # ← この時点では "auto"
if backend == "auto":
preferences = get_gpu_backend_preferences(detected_gpu_type)
if preferences:
gpu_type = backend_to_gpu_env(preferences[0]) # → "vulkan" → "GGML_VK_VISIBLE_DEVICES"
...
AMD GPUの場合preferences[0]は"vulkan"なので、gpu_typeは"GGML_VK_VISIBLE_DEVICES"になります。このキーは_LLAMA_CPP_IMAGESに存在しないため、config.default_image(= ramalama:0.24)にフォールバックします。
第2段階: 実行時の上書き(通常runのみ)
def _do_run(self, args, model):
...
if args.container and not args.dryrun: # ← dryrunだとスキップ!
config = ActiveConfig()
should_pull = config.pull in ["always", "missing", "newer"]
args.image = ensure_image(
config.engine, accel_image(config), should_pull=should_pull, ...
)
通常のrunでは、--backend rocmがconfigに反映された後にaccel_image(config)が再度呼ばれ、正しいイメージ(rocm:0.24)でargs.imageが上書きされます。
しかし**--dryrunではこのブロック全体がnot args.dryrunでスキップされる**ため、第1段階のデフォルト値(ramalama:0.24)がそのまま使われます。
フロー図
[argparse] --image のデフォルト値を評価
└→ default_image() → accel_image() → get_container_image()
backend = "auto"(--backend rocmはまだ未反映)
AMD GPU → preferences[0] = "vulkan"
→ args.image = "ramalama:0.24"
[argparse] コマンドライン引数を解析
└→ --backend rocm が config に反映される
[_do_run]
├─ 通常run: args.image = accel_image(config) ← backendが反映済み → "rocm:0.24" ✓
└─ --dryrun: スキップ → args.image は "ramalama:0.24" のまま ✗
実際の影響
--dryrunは「実際に実行されるコマンドを事前確認する」ためのオプションです。しかし、このバグにより--backend rocmを指定しても出力されるイメージ名がデフォルトのままになるため、dryrunの出力が実際の実行内容と一致しないという問題があります。
通常のrunでは、_do_run内でaccel_image(config)が再度呼ばれ、--backend rocmが反映された状態でイメージ解決が行われるため、正しくrocm:0.24が選択されます。つまり**--dryrunの出力だけが実際の動作と食い違う**状態です。
実行前に--dryrunでコマンドを確認し、そのままコピーして手動実行するようなワークフローでは、間違ったイメージで起動してしまう可能性があります。
修正案
_do_runの--dryrunでもイメージ解決を行うようにすれば、--dryrun出力の不整合は解消できます。
def _do_run(self, args, model):
...
if args.container:
config = ActiveConfig()
should_pull = config.pull in ["always", "missing", "newer"]
if args.dryrun:
args.image = accel_image(config) # pullせずにイメージ名だけ解決
else:
args.image = ensure_image(
config.engine, accel_image(config), should_pull=should_pull, ...
)
v0.25での修正
この問題は、v0.25でPR #2932に含まれるコミット713cd813で修正されていました。
修正のアプローチは、本記事で分析した「第2段階(_do_run内)でdryrunでもイメージ解決する」という方法ではなく、parse_args_from_cmdの中で、引数解析の完了後にargs.imageを再評価するというものです。
修正内容
# The runtime config now carries --backend, so the image the command will
# run can be resolved for real.
if hasattr(args, "image") and not getattr(args, "image_override", False):
args.image = accel_image(config)
parse_args_from_cmdは引数解析を2段階で行います。
-
Phase 1:
get_initial_parser()で--backendなどのグローバルオプションを先行解析し、configに反映 -
Phase 2:
get_parser()でサブコマンドを含む全引数を解析
問題は、Phase 2で--imageのデフォルト値(default_image())が評価される時点では、--backendがconfigに反映済みのように見えて、実はdefault_image()が@lru_cacheでキャッシュされているため、最初の呼び出し時の値がそのまま使われる点です。
この修正では、Phase 2の引数解析とランタイムプラグインへのconfig同期(sync_args_to_runtime_config)が完了した後に、--imageがユーザーから明示指定されていなければ(image_overrideがFalseなら)、accel_image(config)で再評価しています。
さらに、OverrideDefaultActionクラスを拡張して、argparseのデフォルト値とユーザーが明示的に渡した値を区別できるようにしています。
class OverrideDefaultAction(argparse.Action):
dests: set[str] = set()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
OverrideDefaultAction.dests.add(self.dest)
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
setattr(namespace, self.dest + '_override', True)
これにより、--imageを指定しない場合はimage_overrideがFalseのまま → 再評価される、--imageを指定した場合はimage_overrideがTrue → ユーザーの指定が尊重される、という動作になります。
フロー図(修正後)
[Phase 1] get_initial_parser() で --backend rocm を先行解析
└→ config.backend = "rocm"
[Phase 2] get_parser() で全引数を解析
└→ default_image()はキャッシュ済み → args.image = "ramalama:0.25"(古い値)
[sync_args_to_runtime_config] ランタイムconfigに--backendを同期
[再評価] image_override == False なので accel_image(config) を再呼び出し
└→ config.backend = "rocm" が反映済み → args.image = "rocm:0.25" ✓
本記事で提案した_do_run内での修正案は、--dryrunに限定した対症療法でしたが、実際のupstreamの修正はparse_args_from_cmdで引数解析直後に解決するため、--dryrunだけでなく--generateやその他のコードパスも含めて一括で正しいイメージが設定されます。
まとめ
-
--dryrunは--backendオプションをイメージ選択に反映しない(v0.24時点) - 原因: argparseのデフォルト値が
--backendの解析前に評価され、--dryrunでは実行時の上書きがスキップされるため - 通常の
runでは正しいイメージが選択されるが、--dryrunの出力だけが実際の動作と一致しない - v0.25で
713cd813により修正済み:parse_args_from_cmd内で引数解析完了後にargs.imageを再評価する方式