Z-Image-Turbo on AMD Ryzen AI Max+ 395: Local AI Image Generation with Vulkan (Framework Desktop)
Reliably generate images through Open WebUI with Strix Halo
Z-Image-Turbo on AMD Ryzen AI Max+ 395: Local AI Image Generation with Vulkan
My issue
As of today, December 2025, I spent way too long trying to get ComfyUI and Automatic1111 working on my Framework Desktop (Ryzen AI Max+ 395) with Fedora. ROCm just isn’t ready for Strix Halo yet — constant GPU hangs, memory errors, you name it. There’s a more reliable way.
The TL;DR: Skip ROCm, use stable-diffusion.cpp with Vulkan.
What I ran into: ROCm Issues ( & why to avoid it for now)
Issues I’ve encountered:
- Boot loop after GPU memory fault
- BIOS VRAM “too low”
- Integer Overflow with 96GB Allocation (ROCm 6.3.1 issues)
- GPU Hangs during VAE Decoding
- The list goes on.
With my OS (Fedora 43), I haven’t found a stable way to get newer versions of ROCm enabled.
Vulkan to the Rescue
I’m actively using Vulkan for my other llm programs, it’s stable, reliable, and quick with a strong support of folks who use it. With some investigation, I’ve found stable-diffusion.cpp, which ended up being the missing link.
- Vulkan bypasses ROCm entirely using Mesa’s RADV driver, which has excellent RDNA 3.5 support. Much more friendly to the unified memory architecture.
So what did I do to get this working?
Installed docker (if necessary, I already had an installation before)
# Fedora 43
sudo dnf install -y docker docker-compose git wget
# Enable and start Docker
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker
Created a working directory. For me, I wanted to create a a1111_wrapper to work with Open WebUI.
mkdir ~/sd-webui && cd ~/sd-webui
mkdir -p ~/sd-webui/models/{checkpoints,vae,unet,text_encoders,loras,embeddings,upscalers,controlnet}
mkdir -p ~/sd-webui/outputs
mkdir -p ~/sd-webui/a1111_wrapper
Downloaded my Models
I wanted to use Z-Image-Turbo. So I installed the larger quant as I had the vram to spare, but feel free to use whichever GGUF works best for you.
# Diffusion model (Q8_0 for best quality )
cd ~/sd-webui/models/unet/
wget "https://huggingface.co/leejet/Z-Image-Turbo-GGUF/resolve/main/z_image_turbo-Q8_0.gguf"
# Text encoder (Qwen3-4B)
cd ~/sd-webui/models/text_encoders/
wget "https://huggingface.co/leejet/Z-Image-Turbo-GGUF/resolve/main/Qwen3-4B-Instruct-2507-Q4_K_M.gguf"
# VAE
cd ~/sd-webui/models/vae/
wget "https://huggingface.co/leejet/FLUX.1-schnell-GGUF/resolve/main/ae.safetensors"
Docker Configuration
Create ~/sd-webui/Dockerfile , in it includes stable-diffusion.cpp, but also includes a web interface from https://github.com/leejet/sd.cpp-webui.git. I loved using this, it’s simple and works for exactly what I needed.
FROM fedora:41
# Install dependencies
RUN dnf install -y \
git cmake gcc-c++ \
vulkan-loader vulkan-headers vulkan-tools \
mesa-vulkan-drivers mesa-dri-drivers \
python3 python3-pip \
&& dnf clean all
# Clone and build stable-diffusion.cpp
WORKDIR /app
RUN git clone --recursive https://github.com/leejet/stable-diffusion.cpp.git sd.cpp \
&& cd sd.cpp \
&& mkdir build && cd build \
&& cmake .. -DSD_VULKAN=ON \
&& cmake --build . --config Release -j$(nproc)
# Clone web UI
RUN git clone https://github.com/leejet/sd.cpp-webui.git \
&& cd sd.cpp-webui \
&& python3 -m venv venv \
&& ./venv/bin/pip install --upgrade pip \
&& ./venv/bin/pip install -r requirements.txt
# Copy sd binary to webui
RUN cp /app/sd.cpp/build/bin/sd /app/sd.cpp-webui/
WORKDIR /app/sd.cpp-webui
EXPOSE 7860
CMD ["./venv/bin/python", "app.py", "--listen"]
Next, I created ~/sd-webui/docker-compose.yml:
services:
sd-webui:
build:
context: .
dockerfile: Dockerfile
container_name: sd-cpp-webui
ports:
- "7860:7860"
devices:
- /dev/dri:/dev/dri
- /dev/kfd:/dev/kfd
group_add:
- video
- render
volumes:
# Model directories
- ./models/checkpoints:/app/sd.cpp-webui/models/checkpoints
- ./models/vae:/app/sd.cpp-webui/models/vae
- ./models/unet:/app/sd.cpp-webui/models/unet
- ./models/text_encoders:/app/sd.cpp-webui/models/llm
- ./models/loras:/app/sd.cpp-webui/models/loras
- ./models/embeddings:/app/sd.cpp-webui/models/embeddings
- ./models/upscalers:/app/sd.cpp-webui/models/upscale_models
- ./models/controlnet:/app/sd.cpp-webui/models/controlnet
- ./outputs:/app/sd.cpp-webui/outputs
# Config files
- ./config.json:/app/sd.cpp-webui/config.json
- ./prompts.json:/app/sd.cpp-webui/prompts.json
- ./options_cache.json:/app/sd.cpp-webui/options_cache.json
environment:
- AMD_VULKAN_ICD=RADV
- RADV_PERFTEST=gpl
restart: unless-stopped
Configuration
Finally, I created a config file ~/sd-webui/config.json and created empty files for the other configs to populate upon initialization.
{
"llm_dir": "/app/sd.cpp-webui/models/llm/",
"def_sampling": "euler",
"def_steps": 8,
"def_scheduler": "discrete",
"def_cfg": 1,
"def_width": 1024,
"def_height": 1024,
"def_flash_attn": true,
"def_llm": "Qwen3-4B-Instruct-2507-Q4_K_M.gguf",
"def_unet": "z_image_turbo-Q8_0.gguf",
"def_unet_vae": "ae.safetensors",
"def_vae_tiling": true,
"def_vae_tile_overlap": 0.5,
"def_vae_tile_size": 32
}
echo '{}' > ~/sd-webui/prompts.json
echo '{}' > ~/sd-webui/options_cache.json
Important Settings for Z-Image-Turbo
- Make sure the CFG Scale is 1.0
- Set Steps to 8
Let’s run it!
This may take a second.
cd ~/sd-webui
docker compose build
docker compose up -d
After it’s done, open http://localhost:7860 (or http://<your-ip>:7860 from other devices). Everything should be working!
What about Open WebUI Integration?
I use Open WebUI for my local chat engine, and wanted to integrate image generation into it. But, it only supports automatic1111 and comfyUI for local generation. So I created an automatic1111 API wrapper
Steps
Create ~/sd-webui/a1111_wrapper/Dockerfile:
FROM fedora:41
RUN dnf install -y \
git cmake gcc-c++ \
vulkan-loader vulkan-headers vulkan-tools \
mesa-vulkan-drivers mesa-dri-drivers \
python3 python3-pip \
&& dnf clean all
WORKDIR /app
RUN git clone --recursive https://github.com/leejet/stable-diffusion.cpp.git sd.cpp \
&& cd sd.cpp \
&& mkdir build && cd build \
&& cmake .. -DSD_VULKAN=ON \
&& cmake --build . --config Release -j$(nproc)
RUN cp /app/sd.cpp/build/bin/sd /app/
RUN pip install fastapi uvicorn pydantic --break-system-packages
COPY a1111_api.py /app/
WORKDIR /app
EXPOSE 7861
CMD ["python3", "a1111_api.py"]
Create ~/sd-webui/a1111_wrapper/a1111_api.py:
"""AUTOMATIC1111-compatible API wrapper for stable-diffusion.cpp"""
import asyncio
import base64
import os
import subprocess
import tempfile
import uuid
from pathlib import Path
from typing import Optional
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import uvicorn
app = FastAPI(title="SD.cpp A1111 API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Configuration
SD_BINARY = "/app/sd"
MODELS_DIR = "/app/models"
OUTPUTS_DIR = "/app/outputs"
DEFAULT_DIFFUSION_MODEL = os.environ.get("DEFAULT_DIFFUSION_MODEL", "z_image_turbo-Q8_0.gguf")
DEFAULT_VAE = os.environ.get("DEFAULT_VAE", "ae.safetensors")
DEFAULT_LLM = os.environ.get("DEFAULT_LLM", "Qwen3-4B-Instruct-2507-Q4_K_M.gguf")
# Generation state
current_generation = {"progress": 0, "job": None}
class Txt2ImgRequest(BaseModel):
prompt: str
negative_prompt: str = ""
width: int = 1024
height: int = 1024
steps: int = 8
cfg_scale: float = 1.0
seed: int = -1
sampler_name: str = "euler"
scheduler: str = "discrete"
class Txt2ImgResponse(BaseModel):
images: list[str]
parameters: dict
info: str
@app.get("/sdapi/v1/sd-models")
async def get_models():
"""List available models"""
models = []
unet_dir = Path(MODELS_DIR) / "unet"
if unet_dir.exists():
for f in unet_dir.glob("*.gguf"):
models.append({
"title": f.name,
"model_name": f.stem,
"filename": str(f),
})
return models
@app.get("/sdapi/v1/samplers")
async def get_samplers():
"""List available samplers"""
return [
{"name": "euler", "aliases": []},
{"name": "euler_a", "aliases": []},
{"name": "heun", "aliases": []},
{"name": "dpm2", "aliases": []},
{"name": "dpm++2s_a", "aliases": []},
{"name": "dpm++2m", "aliases": []},
{"name": "lcm", "aliases": []},
]
@app.get("/sdapi/v1/schedulers")
async def get_schedulers():
"""List available schedulers"""
return [
{"name": "discrete", "label": "Discrete"},
{"name": "karras", "label": "Karras"},
{"name": "exponential", "label": "Exponential"},
{"name": "sgm_uniform", "label": "SGM Uniform"},
]
@app.get("/sdapi/v1/progress")
async def get_progress():
"""Get generation progress"""
return {
"progress": current_generation["progress"],
"state": {"job": current_generation["job"]},
}
@app.get("/sdapi/v1/options")
async def get_options():
"""Get current options"""
return {
"sd_model_checkpoint": DEFAULT_DIFFUSION_MODEL,
}
@app.post("/sdapi/v1/txt2img")
async def txt2img(request: Txt2ImgRequest):
"""Generate image from text prompt"""
job_id = str(uuid.uuid4())[:8]
current_generation["job"] = job_id
current_generation["progress"] = 0
with tempfile.TemporaryDirectory() as tmpdir:
output_path = Path(tmpdir) / f"{job_id}.png"
cmd = [
SD_BINARY,
"--diffusion-model", str(Path(MODELS_DIR) / "unet" / DEFAULT_DIFFUSION_MODEL),
"--vae", str(Path(MODELS_DIR) / "vae" / DEFAULT_VAE),
"--llm", str(Path(MODELS_DIR) / "llm" / DEFAULT_LLM),
"--prompt", request.prompt,
"--width", str(request.width),
"--height", str(request.height),
"--steps", str(request.steps),
"--cfg-scale", str(request.cfg_scale),
"--sampling-method", request.sampler_name,
"--schedule", request.scheduler,
"--output", str(output_path),
"--vae-tiling",
"--diffusion-fa",
]
if request.negative_prompt:
cmd.extend(["--negative-prompt", request.negative_prompt])
if request.seed >= 0:
cmd.extend(["--seed", str(request.seed)])
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=300,
)
if result.returncode != 0:
raise HTTPException(500, f"Generation failed: {result.stderr}")
if not output_path.exists():
raise HTTPException(500, "Output image not created")
with open(output_path, "rb") as f:
image_b64 = base64.b64encode(f.read()).decode()
current_generation["progress"] = 1.0
return Txt2ImgResponse(
images=[image_b64],
parameters=request.model_dump(),
info="Generation complete",
)
except subprocess.TimeoutExpired:
raise HTTPException(500, "Generation timed out")
finally:
current_generation["job"] = None
current_generation["progress"] = 0
if __name__ == "__main__":
os.makedirs(OUTPUTS_DIR, exist_ok=True)
uvicorn.run(app, host="0.0.0.0", port=7861)
Update the docker-compose.yml we created earlier
Add the API service to your docker-compose.yml:
services:
sd-webui:
# ... (existing config from above)
sd-api:
build:
context: ./a1111_wrapper
dockerfile: Dockerfile
container_name: sd-cpp-api
ports:
- "7861:7861"
devices:
- /dev/dri:/dev/dri
- /dev/kfd:/dev/kfd
group_add:
- video
- render
volumes:
- ./models/unet:/app/models/unet
- ./models/vae:/app/models/vae
- ./models/text_encoders:/app/models/llm
- ./outputs:/app/outputs
environment:
- AMD_VULKAN_ICD=RADV
- RADV_PERFTEST=gpl
- DEFAULT_DIFFUSION_MODEL=z_image_turbo-Q8_0.gguf
- DEFAULT_VAE=ae.safetensors
- DEFAULT_LLM=Qwen3-4B-Instruct-2507-Q4_K_M.gguf
restart: unless-stopped
Configure Open WebUI
- Admin Panel → Settings → Images
- Image Generation Engine: AUTOMATIC1111
- Base URL:
http://<your-ip>:7861 - Verify Connection (should show green checkmark)
- Image Size: 512x512, Steps: 8
We’re done! We now have a reliable system to A) generate images using Z-Image-Turbo with Strix Halo and B) a way to connect this into Open WebUI for integrated image generation.

메타데이터
- post_id
- b577b798b6ca
- slug
- z-image-turbo-on-amd-ryzen-ai-max-395-local-ai-image-generation-with-vulkan-framework-desktop-b577b798b6ca
- url
- https://medium.com/@jmdevita/z-image-turbo-on-amd-ryzen-ai-max-395-local-ai-image-generation-with-vulkan-framework-desktop-b577b798b6ca
- canonical_url
- https://medium.com/@jmdevita/z-image-turbo-on-amd-ryzen-ai-max-395-local-ai-image-generation-with-vulkan-framework-desktop-b577b798b6ca
- author_url
- https://medium.com/@jmdevita
- status
- ok
- fetched_at
- 2026-08-11 04:39:50