← Back to list

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

Julian De Vita · 2025-12-06 18:55 · 40 claps · 5.4 min read
#halo-strix #z-image #framework #open-webui #text-to-image-generation
Open on Medium ↗
Wiki topics: MM · Multimodal & Generative Media 🧘 · Spirituality

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

  1. Admin Panel → Settings → Images
  2. Image Generation Engine: AUTOMATIC1111
  3. Base URL: http://<your-ip>:7861
  4. Verify Connection (should show green checkmark)
  5. 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