Qwen-Image-2.1-Turbo 加速版的教學:文字生圖與局部修改

Qwen-Image-2.1-Turbo 釋出八步生成與圖片編輯權重。本文整理官方環境要求、預存取樣時程、商品圖提示詞與逐張驗收方法,並說明研究授權、CUDA 範例及商用前要確認的條件。

核心架構與技術特點

這個模型基於七十億參數的視覺生成架構,採用三十二層的 Single-Stream DiT 設計。與需要二十五到四十步推理的標準基礎模型不同,Turbo 版本透過特殊的加速機制與預存取樣時程,將去噪步驟大幅縮短至八步。它的預設無分類器引導係數固定為一,省去了額外的前向計算過程,因此能以極快的速度完成推理。

主要功能

模型支援文字生成圖片,能夠產出高達 2K 解析度的畫面。除了文生圖之外,它也具備強大的圖片編輯能力。使用者可以透過自然語言指令、參考圖、塗繪標註或蒙版,對既有圖像進行局部修改、更換背景、增加配件或調整場景,同時保留主體與商品的身份特徵。此外,它也支援原生透明度處理。

應用場景

由於其八步快速出圖的特性,這個模型非常適合需要頻繁反覆修改、快速產出草稿或進行電商商品圖批次處理的工作流程。開發者通常會透過 Hugging Face 的 Diffusers 套件以及 ComfyUI 等介面來載入與部署這款模型。

兩者的核心差異

Qwen/Qwen-Image-2.1-Turbo 是 Qwen/Qwen-Image-2.1 的蒸餾加速版本。兩者在底層使用了相同的七十億參數 Single-Stream DiT 視覺生成架構,但推理機制與速度上有顯著不同:

比較項目Qwen/Qwen-Image-2.1(標準原版)Qwen/Qwen-Image-2.1-Turbo(加速版)
推理步數需要 25 至 40 步僅需 8 步(內建預設排程)
無分類器引導(CFG)需要設定 CFG > 1(需雙倍前向計算)預設 CFG = 1(嵌入權重,無額外計算)
生成速度較慢,但控制精細度高快約 5 至 8 倍,非常適合快速預覽與大量生成
提示詞遵循能力極佳,適合長文字或複雜多指令提示稍遜於原版,極複雜指令可能有些微調偏差

Turbo 可以完全覆蓋原版的所有功能嗎

答案是:在功能支援上可以完全覆蓋,但在極限品質與控制力上無法完全取代原版。

功能範疇完全重疊:

Turbo 版本完全繼承了原版的所有核心功能,包括文字生成圖片、透明背景 RGBA 圖層生成、多圖參考編輯、塗繪與蒙版局部修改、以及人物與商品特徵保留。在 Diffusers 程式庫中,兩者也是使用同一個管道載入。

無法完全取代原版的特定場景:

  • 極複雜提示詞:Turbo 版本透過蒸餾技術將引導資訊固化在權重中。當提示詞包含非常長且繁複的多層次指令時,標準原版在 40 步與高 CFG 下的細節表現仍會比 Turbo 更精準。
  • 自由調整參數:原版允許使用者自由調整係數與微調取樣步數,以在創造力與文字貼合度之間找到最佳平衡。Turbo 版本的排程與係數已被鎖定與最佳化,調參彈性較低。

使用建議

  • 如果需要高效率生產、電商批次處理、即時互動生成或快速草稿設計,應優先選擇 Turbo。
  • 如果需要處理非常精細的藝術設計、極度複雜的邏輯指令,或是追求極致細節的最終成品,建議保留原版。

Q: 顯示卡 RTX 4050 Laptop 6GB VRAM 可以跑的動?

以預設的標準精度(BF16 或 FP16)來看,Qwen-Image-2.1 系列模型大約需要十六到二十九 GB 以上的顯示記憶體,因此 RTX 4050 Laptop 的六 GB 顯示記憶體無法直接流暢運行原生日誌與完整精度。

不過,如果你透過以下幾種優化方式,這張顯示卡還是有機會可以嘗試本地端運行:

  • 降低精度與量化:透過 GGUF 量化版本或 INT4、INT8 壓縮權重,將模型體積縮小,讓記憶體需求降至六 GB 甚至更低,但這會犧牲部分生成畫質。
  • 善用系統記憶體(RAM):如果筆電的共用記憶體與主記憶體足夠大,部分運行框架支援將部分運算卸載到 CPU 或系統記憶體,只是生成速度會變得很慢。
  • 雲端算力替代:如果本地運行經常遇到顯示記憶體不足(Out of Memory)的錯誤,建議改用 Google Colab 或雲端 GPU 服務來執行。

Q: 如何找到降低精度與量化的版本?

在 Hugging Face 或開源社群尋找像 Qwen-Image-2.1 系列這種大型 DiT 模型的量化(Quantized)或降低精度版本時,主要有以下幾條途徑與搜尋方法:

一、 直接在 Hugging Face 搜尋對應的量化庫

量化版本通常不會放在官方原版的 repo 中,而是由社群創作者(如 Unsloth、city96、leejet 等)轉換後發布。你可以直接在 Hugging Face 的搜尋列輸入以下關鍵字:

  • GGUF 版本(最適合 6GB VRAM 使用):
    • 搜尋:Qwen-Image-2.1 GGUF 或 Qwen-Image-2.1-Turbo GGUF
    • 核心量化層級選擇:
      • Q4_K_M(約 4.2 GB):最推薦 RTX 4050 6GB 使用,模型可以直接完全載入顯示記憶體。
      • Q5_0 / Q5_K_M(約 5.0 – 5.3 GB):細節稍微提升,但可能會用到少許系統記憶體(RAM)做緩衝。
  • INT8 / NF4 版本(Comfy-Org 官方或社群微調):
    • 搜尋:Qwen-Image-2.1-Turbo-INT8 或 qwen_image_2.1_turbo_int8_convrot
    • 這是直接將權重壓成 INT8 格式的 safetensors 檔案,適合傳統 ComfyUI 流程。

二、 搭配 ComfyUI 運行 GGUF 量化版(最建議的做法)

若要在 RTX 4050 上順利運行 GGUF 模型,單靠預設的 ComfyUI 節點可能會遇到「不支援架構」的錯誤。推薦使用支援該模型架構的擴充元件與組件:

  1. 安裝自訂節點:使用 leejet 維護的 ComfyUI-GGUF 套件(比舊版 city96 能更完善支援 Qwen 2.1 架構)。
  2. 下載配套檔案:
    • UNet / Diffusion 模型: 放至 ComfyUI/models/unet/,選擇下載好的 .gguf 檔案(例如 qwen-image-2.1-Q4_K_M.gguf)。
    • Text Encoder(文本編碼器):Qwen-Image 2.1 搭配的是 8B 的文本編碼器。為了省記憶體,同樣建議尋找量化過的文字編碼器,如 qwen3vl_8b_w4a8.safetensors 或 qwen3vl_8b_int8 並放入 models/text_encoders/。
  3. 替換節點:在 ComfyUI 工作流中,將原本的 Load Diffusion Model 替換為 Unet Loader (GGUF) 即可載入。

三、 使用整合型工具(如 Ollama / LM Studio 等,若支援)

如果不想手動設定 ComfyUI 工作流,也可以關注像是 Draw Things(Mac/iOS 支援,部分 Windows 框架亦有移植)或 Civitai 網站上是否有創作者上傳整理好的單一打包模型,通常搜尋欄位輸入 Qwen-Image-2.1 Turbo Quantized 即可找到相關資源。


https://huggingface.co/unsloth/Qwen-Image-2.1-Turbo-GGUF

which version best for my RTX 4050 Laptop 6GB RAM?

▋ 最推薦的版本:Q4_K_M (4.2 GB)

對於 RTX 4050 Laptop 6GB 顯示記憶體來說,Q4_K_M 是兼顧品質與運行成功率的最佳首選。

▋ 各版本在 6GB 顯示記憶體下的實測建議與分析

在生成圖片時,顯示記憶體(VRAM)除了要存放主要的 DiT 模型權重之外,還必須留出空間給:

  1. Text Encoder(文本編碼器,如 Qwen3-VL 8B): 需額外佔用約 0.7 – 2 GB 顯示記憶體。
  2. VAE 解碼器與 CUDA 計算緩衝區(Compute Buffers): 約需 1.0 – 1.5 GB。

因此,模型的檔案大小不能剛好等於 6 GB,必須留出至少 1.5 – 2 GB 的動態開銷空間。

  • Q4_K_M (4.2 GB) —— 最平衡首選
    • 分析:模型本身佔用約 4.2 GB。搭配 ComfyUI 的模型卸載(CPU Offloading)機制,可以在 6GB VRAM 內剛好塞下,避免發生 OOM(Out of Memory)記憶體溢位崩潰。
    • 畫質損耗: Unsloth 的 Dynamic 4-bit 量化對畫面細節與文字遵循能力保留得相當完整,幾乎感受不出明顯品質下降。
  • Q4_K_S (3.91 GB) —— 備用方案(記憶體不夠時使用)
    • 分析: 檔案更小,如果你的系統背景程式佔用了較多 VRAM(例如開著 Chrome 或多個螢幕),導致 Q4_K_M 報錯,請改用此版本。
  • Q5_K_S / Q5_K_M (4.5 GB – 5.39 GB) —— 邊緣臨界,易爆 VRAM
    • 分析:4.5 GB 以上的檔案加上 VAE 與編碼器開銷後,非常容易超過 6GB 限制。若要硬跑,系統會被迫將部分模型卸載到電腦的主記憶體(RAM),導致生成速度變得很慢。
  • Q6_K / Q6_K_XL (6.27 GB – 6.72 GB) —— 完全不建議
    • 分析:單模型檔案大小就已超過你的顯示記憶體上限。

▋ 6GB VRAM 順暢運行的關鍵設定

  1. 圖形解析度:請先從 1024×1024 開始生成。切勿一開始就設為 2048×2048。
  2. Text Encoder (文本編碼器):建議下載量化過的文字編碼器(如 qwen3vl_8b_w4a8 或 int8),或者在 ComfyUI 中開啟 CPU Text Encoder 模式,把文字處理丟給電腦主記憶體。
  3. VAE 設定:在 ComfyUI 的 VAE Loader 設定中開啟 Tiled VAE(區塊解碼),可以大幅節省生成最後階段的 VRAM 峰值用量。

Q: 如何串接 AI agent CLI 生圖

要把文字互動介面與本地端生圖模型串接起來,主要有兩個常見且可行的解法。

第一種是透過 ComfyUI 內建的 API 伺服器來串接。ComfyUI 啟動後會在背景提供一套完整的網頁伺服器與 API 功能。你可以在終端機裡寫一個簡單的 Python 程式或是用 curl 指令,把提示詞打包成 JSON 格式發送到 ComfyUI 的連接埠,讓它在背景自動生成圖片並回傳檔案路徑。驗證這個步驟是否成功的方法是,打開瀏覽器確認網頁伺服器有正常回應。

第二種是自己寫一個輕量化的轉接層。有些 agent cli 工具預設只能對接類似 OpenAI 的文字或圖片生成 API。你可以用 Python 的 FastAPI 寫一個簡單的伺服器,對外提供標準的 API 介面,當收到終端機傳來的生圖請求時,程式再自動去呼叫底層的生圖工具。驗證這個步驟是否成功的方法是,用終端機發送一筆測試請求,確認伺服器能夠正確接收並處理。

give me solution for my RTX 4050 Laptop 6GB VRAM

你的顯示記憶體只有 6GB ,直接執行官方的原始程式碼一定會立刻跳出記憶體不足的錯誤。為了讓程式碼能在你的電腦上順利跑完,我們必須修改幾個關鍵設定,包含大幅降低圖片解析度以及開啟 CPU 記憶體卸載功能。

這是一份修改過後的 Python 程式碼,可以讓你在 RTX 4050 上面嘗試執行:

Python

import torch
from diffusers import QwenImage21Pipeline

pipe = QwenImage21Pipeline.from_pretrained(
    "Qwen/Qwen-Image-2.1-Turbo",
    torch_dtype=torch.float16,
)

pipe.enable_model_cpu_offload()

prompt = "A vertically oriented educational chemistry study poster fills the frame, designed like a hand-drawn classroom notes page with a clean white background, thick dark navy-blue outer border, and thin inner navy rectangular border lines. The overall aesthetic is colorful, high-contrast, and hand-lettered in a signature Topper style using marker and watercolor effects. At the top left, a small header in black handwriting reads \"Topper’s Notes\", accented by short decorative navy strokes radiating from it. Centered across the top is the main lesson heading in large stylized black-and-yellow bold lettering: \"Science – Chapter 1:\" followed below by the much larger title \"Chemical Reactions and Equations\". The title has energetic yellow highlighter strokes underneath and small sparkle doodles nearby. At the top right, inside a small outlined page-number box, the label reads \"Page X\". Below the header, a wide rounded rectangle framed in dark navy contains a pink brush-stroke highlight banner with the subheading \"IMPORTANT CHEMICAL EQUATION:\" in black. Inside the same equation panel, the reaction is written very large as \"Fe + CuSO₄ → FeSO₄ + Cu\". The symbol \"Fe\" is highlighted in bright orange with an underline, \"CuSO₄\" is vivid blue and underlined, \"FeSO₄\" is bright green and underlined, and \"Cu\" is metallic bronze and underlined. Beneath the left side of the equation is a lavender rounded label connected by a leader line to the iron component; it says \"Reactants\" and below it \"(before the reaction)\", with \"Reactants\" colored purple and underlined. Beneath the right side is a matching pale-green rounded label connected by a leader line to the products; it says \"Products\" and below it \"(after the reaction)\", with \"Products\" colored green and underlined. Along the lower part of this equation box are stylized ball-and-stick molecule models and atomic orbital drawings: a silver-gray sphere labeled \"Fe\" sits left of center, another cluster shows gray, red, and white spheres representing sulfate groups around blue and green atoms, and a copper-colored sphere appears near the right. Small atom-and-orbit icons sit along the bottom edge of the panel in blue, green, and brown outline styles. The middle portion is enclosed by a large dashed green rounded rectangle titled \"REACTIVITY CONCEPT\" in green uppercase letters with decorative rays. On the left side of this section, a stepped vertical reactivity-series ladder is drawn with alternating colored horizontal blocks. From top to bottom it lists \"K\", \"Na\", \"Ca\", \"Mg\", \"Al\", \"Zn\", \"Fe\" with a green oval callout reading \"(More reactive)\", then continues downward with \"Cu\", \"Ag\", \"Hg\", \"Pt\", \"Au\" with an orange oval callout reading \"(Less reactive)\". Each step is marked by a dark upward-pointing triangular arrow, showing decreasing reactivity downward. In the central-right area, full body text as follows: \"Iron (Fe) is higher in the reactivity series than Copper (Cu). This makes Iron more reactive.\" Below this definition is a visual arrow sequence of overlapping circles: orange \"Fe\" overlaps blue \"Cu\", followed by a red arrow pointing toward the phrase \"→ More displacement ↓ less reactivity\". Under it, a prominent statement in dark handwriting reads \"More reactive element displaces less reactive element\", emphasized with a bright pink marker underline. On the right, a shield-like badge with a navy outline and light interior contains the bold contrasting font label \"DISPLACEMENT REACTION\". Below the badge, full body text as follows: \"A more reactive element displaces a less reactive element from its compound.\" The badge includes an internal miniature illustration of circular electron shells and colored particles, reinforcing the chemistry theme. Directly below the reactivity concept box, a smaller rounded rectangular table titled \"PROPERTY COMPARISON\" compares Iron and Copper. The table has three columns: \"IRON (Fe)\", \"COPPER (Cu)\", and a central property column. Row one reads \"Reactivity series position\" with Iron shown as \"Higher (more reactive)\" and Copper shown as \"Lower (less reactive)\". Row two reads \"Displaces hydrogen from acids\" with Iron marked \"Yes\" and Copper marked \"No\". Row three reads \"Displaced by other metals\" with Iron shown as \"Displaced by Zn, Mg, Al, etc.\" and Copper shown as \"Displaced by Fe, Zn, Mg, etc.\". Row four reads \"Oxidation tendency\" with Iron shown as \"Gets oxidized easily\" and Copper shown as \"Does not get oxidized easily\". Row five reads \"Useful property\" with Iron described as \"Used where strength & corrosion resistance needed\" and Copper described as \"Used where conductivity & corrosion resistance needed\". The table uses thin navy grid lines, orange tinting for the iron column, pale blue tinting for the copper column, black condensed handwriting, and green checkmarks next to positive traits and red crosses next to limitations. The lower half introduces a large illustrated experiment section with a cream highlight behind the title \"BEFORE/AFTER IRON NAIL EXPERIMENT\" in bold dark navy uppercase letters. The section is split visually into a left \"Before\" state and a right \"After\" state. On the left, a beaker filled with bright blue solution contains a shiny silver iron nail standing diagonally. The beaker label reads \"CuSO₄(aq)\" in blue, and the bottom label near the nail reads \"Fe(s)\". A rounded annotation box beside the beaker says \"Initial State: Blue Solution & Silver Nail\". Two glass test tubes stand to the right of this beaker: the upper test tube is filled with blue solution and labeled \"CuSO₄(aq)\", while the lower one contains a pale green liquid. Red and blue directional arrows connect these visual elements to explanations: one red arrow points toward the nail with the text \"Copper gets deposited\", and one blue arrow points toward the liquid with the text \"Solution turns pale green\". On the right side of the experiment section, another beaker shows the after state with the iron nail now coated in brown copper deposits. Its solution is faded to a pale greenish-blue, and the beaker label reads \"FeSO₄(aq)\" in green. A nearby rounded annotation box says \"Final State: Pale Green Solution & Brown Coated Nail\". To the far right are additional test tubes and ion symbols: a test tube containing blue liquid labeled \"CuSO₄(aq)\" with the note \"Blue colour due to Cu²⁺ ions\", and a lower test tube containing pale green liquid labeled \"FeSO₄(aq)\" with the note \"Pale green due to Fe²⁺ ions\". Beside them are ion diagrams labeled \"Cu²⁺(aq)\" and \"Fe²⁺(aq)\", shown as colored charged particles encircled by simple electron-shell rings; the copper ion uses blue particles and the iron ion uses green particles. Small sparkles, arrows, and dashed guide lines emphasize the transformation from blue copper sulfate solution to pale green iron sulfate solution. Near the bottom, a horizontal summary strip has a blue star icon followed by the heading \"SUMMARY\". Full body text as follows: \"In this reaction, Iron (Fe) displaces Copper (Cu) from Copper Sulphate (CuSO₄) solution to form Iron(II) Sulphate (FeSO₄) solution and Copper (Cu).\" To the lower left, a bordered key-points box titled \"KEY POINTS\" lists three starred bullet statements: \"Iron is above copper in the reactivity series.\", \"Fe displaces Cu from CuSO₄ solution.\", and \"Result: Blue solution turns pale green; copper metal deposits on iron nail.\" At the very bottom, inside the footer border, spaced text reads \"CHEMISTRY • CHANGE IS EVERYWHERE • TOPPER’S NOTES\", flanked by small leaf-like ornaments and thin horizontal divider lines. The entire page uses dark navy borders and headings, bright highlighter strokes, watercolor shading, realistic glassware illustrations, and playful scientific doodles to present a chemistry lesson clearly and attractively."

image = pipe(
    prompt=prompt,
    width=512,
    height=768,
    use_kv_cache=True,
    generator=torch.Generator("cpu").manual_seed(42),
).images[0]

image.save("t2i.png")

這段程式碼做了三個重要的調整。第一是把圖片解析度從原本極大的尺寸縮小成 512 乘以 768 像素,因為原本的尺寸需要十六 GB 以上的顯示記憶體才能運作。第二是加入了記憶體卸載指令,當顯示記憶體快滿時,會自動把運算暫存搬到電腦的主記憶體。第三是把資料型態改成了 float16 ,這樣可以省下一半的記憶體空間。

因為這張顯示卡的記憶體容量有限,啟動記憶體卸載之後,生圖的速度會變得非常慢,需要耐心等待程式執行完畢。如果執行時還是出現記憶體不足的錯誤,可以把解析度再調得更小一點。


Q: 上述範例 512×768 會占用多少記憶體, 我如果下載 5-bit mode: Q5_K_S (4.5 GB), 會造成 OOM ?

在 512x768 解析度下,圖片本身的潛在空間與運算暫存區大約會佔用 1 GB 到 1.5 GB 的顯示記憶體。

如果下載 5-bit 的 Q5_K_S 版本(檔案大小約 4.5 GB),在 RTX 4050 的 6 GB 顯示記憶體上極有可能會發生 OOM 錯誤,或是被迫將大量運算卸載到系統記憶體,導致生圖速度變得極慢。

原因在於 6 GB 顯示記憶體扣掉 4.5 GB 的模型檔案後,只剩下大約 1.5 GB 的空間。然而生圖過程還需要載入文字編碼器與解碼器,這些元件加起來很容易超過 6 GB 的極限。

建議還是選擇檔案更小的 4-bit 版本,例如 Q4_K_M 或 Q4_K_S,才能在 6 GB 顯示記憶體上比較穩定地運行。


改寫範例scrpt 為 resetful api + fastapi, input prompt + output target filepath

I will now generate the FastAPI script for running the Qwen-Image-2.1-Turbo model as a RESTful service.

I have created the FastAPI script qwen_api_server.py.

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
import torch
from diffusers import QwenImage21Pipeline
import os

app = FastAPI(
    title="Qwen-Image-2.1-Turbo REST API",
    description="A lightweight FastAPI service to generate images via Qwen-Image-2.1-Turbo and save them to a specified target file path.",
    version="1.0.0"
)

# Global pipeline variable to hold the model in memory
pipe = None

@app.on_event("startup")
def load_model():
    global pipe
    print("Loading Qwen-Image-2.1-Turbo model into GPU memory...")
    try:
        pipe = QwenImage21Pipeline.from_pretrained(
            "Qwen/Qwen-Image-2.1-Turbo",
            torch_dtype=torch.bfloat16,
        ).to("cuda")
        # Uncomment the line below if you run into 6GB VRAM OOM issues:
        # pipe.enable_model_cpu_offload()
        print("Model loaded successfully!")
    except Exception as e:
        print(f"Failed to load model: {e}")
        raise e

class GenerateRequest(BaseModel):
    prompt: str = Field(..., description="The text description for image generation.")
    output_path: str = Field(..., description="The target file path where the generated image will be saved.")
    width: int = Field(512, description="Image width (recommended lower for 6GB VRAM, e.g., 512 or 768).")
    height: int = Field(768, description="Image height (recommended lower for 6GB VRAM, e.g., 768 or 1024).")
    seed: int = Field(42, description="Random seed for reproducibility.")

@app.post("/generate", status_code=status.HTTP_200_OK)
def generate_image(req: GenerateRequest):
    global pipe
    if pipe is None:
        raise HTTPException(status_code=500, detail="Model is not loaded yet.")
    
    try:
        # Ensure output directory exists if a subdirectory is provided
        output_dir = os.path.dirname(req.output_path)
        if output_dir and not os.path.exists(output_dir):
            os.makedirs(output_dir, exist_ok=True)

        # Run inference
        image = pipe(
            prompt=req.prompt,
            width=req.width,
            height=req.height,
            use_kv_cache=True,
            generator=torch.Generator("cpu").manual_seed(req.seed),
        ).images[0]

        # Save to target filepath
        image.save(req.output_path)

        return {
            "status": "success",
            "message": f"Image successfully generated and saved.",
            "output_path": req.output_path
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Image generation failed: {str(e)}")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("qwen_api_server:app", host="127.0.0.1", port=8000, reload=False)

How to use it:

  1. Run the server using: python qwen_api_server.py
  2. You can send a POST request from your CLI agent (like pi agent, agy cli, or curl) to [http://127.0.0.1:8000/generate](http://127.0.0.1:8000/generate) with a JSON body containing your prompt and output_path.

Let me know if you want an example curl command or client script to test this API!


FastAPI script for image editing using the Qwen-Image-2.1-Turbo pipeline, incorporating memory optimizations for your RTX 4050 6GB VRAM.

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
import torch
from diffusers import QwenImage21Pipeline
from diffusers.utils import load_image
import os

app = FastAPI(
    title="Qwen-Image-2.1-Turbo Image Editing REST API",
    description="A FastAPI service for image-to-image / image editing using Qwen-Image-2.1-Turbo, optimized for 6GB VRAM.",
    version="1.0.0"
)

pipe = None

@app.on_event("startup")
def load_model():
    global pipe
    print("Loading Qwen-Image-2.1-Turbo pipeline for image editing into GPU memory...")
    try:
        pipe = QwenImage21Pipeline.from_pretrained(
            "Qwen/Qwen-Image-2.1-Turbo",
            torch_dtype=torch.float16, # Using float16 to conserve VRAM
        )
        
        pipe.enable_model_cpu_offload()
        print("Model and CPU offload successfully configured!")
    except Exception as e:
        print(f"Failed to load model: {e}")
        raise e

class EditRequest(BaseModel):
    prompt: str = Field(..., description="The editing text prompt describing the transformation.")
    input_image_path_or_url: str = Field(..., description="Local file path or URL of the source image/sketch.")
    output_path: str = Field(..., description="The target file path where the edited image will be saved.")
    width: int = Field(1024, description="Image width (scaled down to 1024 or 512 for 6GB VRAM safety).")
    height: int = Field(1024, description="Image height (scaled down to 1024 or 512 for 6GB VRAM safety).")
    seed: int = Field(20261009, description="Random seed for reproducibility.")

@app.post("/edit-image", status_code=status.HTTP_200_OK)
def edit_image(req: EditRequest):
    global pipe
    if pipe is None:
        raise HTTPException(status_code=500, detail="Model is not loaded yet.")
    
    try:
        if req.input_image_path_or_url.startswith("http://") or req.input_image_path_or_url.startswith("https://"):
            input_image = load_image(req.input_image_path_or_url).convert("RGBA")
        else:
            if not os.path.exists(req.input_image_path_or_url):
                raise HTTPException(status_code=400, detail=f"Input image path not found: {req.input_image_path_or_url}")
            input_image = load_image(req.input_image_path_or_url).convert("RGBA")

        # Ensure output directory exists
        output_dir = os.path.dirname(req.output_path)
        if output_dir and not os.path.exists(output_dir):
            os.makedirs(output_dir, exist_ok=True)

        image = pipe(
            prompt=req.prompt,
            image=input_image,
            width=req.width,
            height=req.height,
            use_kv_cache=True,
            generator=torch.Generator("cpu").manual_seed(req.seed),
        ).images[0]

        # Save to target filepath
        image.save(req.output_path)

        return {
            "status": "success",
            "message": "Image editing completed and saved successfully.",
            "output_path": req.output_path
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Image editing failed: {str(e)}")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("qwen_edit_api:app", host="127.0.0.1", port=8000, reload=False)

Q: /generate api and /edit-image api 怎麼寫會比較好, 如果後續還有其他 API 要增加.

如果要讓 FastAPI 專案未來方便擴充更多 API,建議採用模組化設計與依賴注入的概念。

第一點是使用 APIRouter 來拆分路由。不要把所有端點都寫在 main.py 裡面。可以建立一個 routers 資料夾,把生圖功能放在 routers/generation.py,把圖片編輯放在 routers/editing.py,最後再用 app.include_router 整合進主程式。

第二點是使用依賴注入管理模型實體。因為模型佔用大量顯示記憶體,不適合每次請求都重新載入。可以在程式啟動時初始化一次管線,透過 FastAPI 的 Depends 或全域狀態共用同一個 pipeline 執行個體。

第三點是統一請求與回應格式。未來如果加入文字生成或其他功能,輸入與輸出的結構可以用 Pydantic 模型嚴格定義,這樣 agent 呼叫時才能確保欄位正確。

專案資料夾結構可以這樣規劃:

qwen_img_fastapi/
│
├── main.py
├── services/
│   └── model_loader.py
└── routers/
    ├── generation.py
    └── editing.py

透過這種架構,未來如果需要新增其他 API,只要在 routers 裡面多寫一支檔案,並在 main.py 註冊即可,維護起來會更有條理。

main.py

from fastapi import FastAPI
from routers import generation, editing

app = FastAPI(
    title="Qwen-Image-2.1-Turbo Modular REST API",
    description="A modular FastAPI service for image generation and editing using Qwen-Image-2.1-Turbo, optimized for RTX 4050 6GB VRAM.",
    version="1.0.0"
)

# Include routers for different capabilities
app.include_router(generation.router)
app.include_router(editing.router)

@app.get("/", tags=["Root"])
def root():
    return {
        "status": "online",
        "message": "Welcome to Qwen-Image-2.1-Turbo API. Check /docs for Swagger UI documentation."
    }

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=False)

services/model_loader.py

import torch
from diffusers import QwenImage21Pipeline

# Global container for the shared pipeline
_pipe = None

def get_pipeline() -> QwenImage21Pipeline:
    global _pipe
    if _pipe is None:
        print("Loading Qwen-Image-2.1-Turbo pipeline into memory...")
        try:
            _pipe = QwenImage21Pipeline.from_pretrained(
                "Qwen/Qwen-Image-2.1-Turbo",
                torch_dtype=torch.float16,
            )
            # Enable CPU offloading to prevent OOM errors on 6GB VRAM GPUs
            _pipe.enable_model_cpu_offload()
            print("Pipeline successfully loaded and CPU offload enabled!")
        except Exception as e:
            print(f"Failed to load pipeline: {e}")
            raise e
    return _pipe

routers/generation.py

from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel, Field
import torch
import os
from services.model_loader import get_pipeline

router = APIRouter(prefix="/generation", tags=["Image Generation"])

class GenerateRequest(BaseModel):
    prompt: str = Field(..., description="The text description for image generation.")
    output_path: str = Field(..., description="The target file path where the generated image will be saved.")
    width: int = Field(512, description="Image width (keep low, e.g. 512 or 768 for 6GB VRAM).")
    height: int = Field(768, description="Image height (keep low, e.g. 768 or 1024 for 6GB VRAM).")
    seed: int = Field(42, description="Random seed for reproducibility.")

@app.post("/text-to-image" if False else "/generate", status_code=status.HTTP_200_OK)
def generate_image(req: GenerateRequest):
    try:
        pipe = get_pipeline()
        
        output_dir = os.path.dirname(req.output_path)
        if output_dir and not os.path.exists(output_dir):
            os.makedirs(output_dir, exist_ok=True)

        image = pipe(
            prompt=req.prompt,
            width=req.width,
            height=req.height,
            use_kv_cache=True,
            generator=torch.Generator("cpu").manual_seed(req.seed),
        ).images[0]

        image.save(req.output_path)

        return {
            "status": "success",
            "message": "Image generated successfully.",
            "output_path": req.output_path
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")

routers/editing.py

from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel, Field
import torch
import os
from diffusers.utils import load_image
from services.model_loader import get_pipeline

router = APIRouter(prefix="/editing", tags=["Image Editing"])

class EditRequest(BaseModel):
    prompt: str = Field(..., description="The editing text prompt describing the transformation.")
    input_image_path_or_url: str = Field(..., description="Local file path or URL of the source image/sketch.")
    output_path: str = Field(..., description="The target file path where the edited image will be saved.")
    width: int = Field(1024, description="Image width.")
    height: int = Field(1024, description="Image height.")
    seed: int = Field(20261009, description="Random seed for reproducibility.")

@router.post("/edit-image", status_code=status.HTTP_200_OK)
def edit_image(req: EditRequest):
    try:
        pipe = get_pipeline()

        if req.input_image_path_or_url.startswith("http://") or req.input_image_path_or_url.startswith("https://"):
            input_image = load_image(req.input_image_path_or_url).convert("RGBA")
        else:
            if not os.path.exists(req.input_image_path_or_url):
                raise HTTPException(status_code=400, detail=f"Input image not found: {req.input_image_path_or_url}")
            input_image = load_image(req.input_image_path_or_url).convert("RGBA")

        output_dir = os.path.dirname(req.output_path)
        if output_dir and not os.path.exists(output_dir):
            os.makedirs(output_dir, exist_ok=True)

        image = pipe(
            prompt=req.prompt,
            image=input_image,
            width=req.width,
            height=req.height,
            use_kv_cache=True,
            generator=torch.Generator("cpu").manual_seed(req.seed),
        ).images[0]

        image.save(req.output_path)

        return {
            "status": "success",
            "message": "Image editing completed successfully.",
            "output_path": req.output_path
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Image editing failed: {str(e)}")

Q: 為什麼執行 main.py 之後, 畫面顯示還要下載其他檔案?

這是因為一個完整的文生圖 Pipeline(QwenImage21Pipeline)不只有 Transformer 主模型,還包含其他必備元件:

1. 為什麼本機有了 .gguf 還在下載?

在 model_loader.py:35-39 中:

# 1. 這裡載入的是您本機已有的 Transformer 權重 (約 4GB 的 GGUF)
transformer = QwenImage21Transformer2DModel.from_single_file(GGUF_MODEL_PATH, ...)

# 2. 這裡組裝整個 Pipeline:
_pipe = QwenImage21Pipeline.from_pretrained(
    "Qwen/Qwen-Image-2.1-Turbo",
    transformer=transformer,   # <--- Transformer 替換成本地的 GGUF
    torch_dtype=torch.bfloat16,
)

文生圖 Pipeline 除了 Transformer(降噪核心)之外,還需要:

  1. Text Encoder(文字編碼器 / Tokenizer):負責把您的英文 Prompt 轉成模型看得懂的語意向量。
  2. VAE(變分自編碼器):負責將潛空間特徵解碼還原成真實的 PNG/JPEG 像素圖片。
  3. Scheduler / Config 設定檔:去噪步數調度器。

因為您的專案目錄目前只有 Qwen/qwen-image-2.1-turbo-Q4_K_M.gguf(只有 Transformer 本身),所以 Hugging Face Diffusers 會自動去 HuggingFace 官方下載其餘的文字編碼器(Text Encoder)與 VAE。
──────

2. 需要下載多少?之後每次都要下載嗎?

• 只需要下載這一次:下載後的檔案會保存在本機快取目錄(C:\Users\max32.cache\huggingface\hub)。下一次再啟動或呼叫 API 時,就會直接讀取快取,不會再次重複下載。
• 檔案大小:主要是 Text Encoder 與 VAE,下載量約數 GB。

等待這次背景下載完畢後,模型就會自動完成組裝並產出圖片!


在 Diffusers 架構中,每個 Pipeline 需要下載哪些元件是由倉庫根目錄的 model_index.json 定義的。
對於 Qwen/Qwen-Image-2.1-Turbo,它需要的資料夾元件如下:
• 官方倉庫頁面:https://huggingface.co/Qwen/Qwen-Image-2.1-Turbo/tree/main
──────

一、您需要手動下載的檔案清單

因為您已經有 transformer(即您本機的 qwen-image-2.1-turbo-Q4_K_M.gguf),不需要 下載倉庫中的 transformer/ 資料夾(這部分就佔了十幾 GB)。
您只需要下載其餘的 4 個元件資料夾 與 根目錄設定檔:

1. 根目錄設定檔

• model_index.json

2. text_encoder/ 資料夾

│ 文字編碼器(Qwen3-VL 文本主幹),主要權重檔比較大:

• config.json
• generation_config.json
• model.safetensors(或是 model.safetensors.index.json 與相關的 .safetensors 分卷)

3. processor/ 資料夾(含 Tokenizer)

• chat_template.json
• preprocessor_config.json
• processor_config.json
• tokenizer_config.json
• vocab.json / merges.txt(或相關的 tokenizer 檔案)

4. vae/ 資料夾

│ 負責把潛向量轉為實際圖片:
• config.json
• diffusion_pytorch_model.safetensors

5. scheduler/ 資料夾

• scheduler_config.json
──────

二、手動下載後如何放置與修改程式碼?

建議在專案中建立一個本地目錄(例如 models/Qwen-Image-2.1-Turbo),並把上述檔案依照相同結構放好:

C:\Users\max32\Downloads\qwen_img_fastapi\
 ├── Qwen\
 │    └── qwen-image-2.1-turbo-Q4_K_M.gguf
 └── models\
      └── Qwen-Image-2.1-Turbo\
           ├── model_index.json
           ├── text_encoder\
           ├── processor\
           ├── vae\
           └── scheduler\

然後在 model_loader.py 將 BASE_MODEL_ID 改成此本機路徑:

BASE_MODEL_ID = os.path.join(
    os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
    "models",
    "Qwen-Image-2.1-Turbo"
)

修改後,Pipeline 就不會連網下載任何東西,而是直接從本機硬碟快速載入!
──────

三、推薦的高速下載方式(比瀏覽器手動點一個個檔案快)

如果您使用瀏覽器一個個點比較繁瑣,推薦使用支援多執行緒並發下載的工具:

方式 A:使用 hf-transfer(推薦,通常可跑滿寬頻)

在終端機安裝並開啟加速:

pip install -U hf-transfer huggingface_hub
$env:HF_HUB_ENABLE_HF_TRANSFER = "1"

接著使用指令直接下載排除 transformer 以外的所有檔案至本機資料夾:

huggingface-cli download Qwen/Qwen-Image-2.1-Turbo --exclude "transformer/*" --local-dir models/Qwen-Image-2.1-Turbo

方式 B:使用 IDM / 多線程下載器

如果網路對 Hugging Face 有限制,亦可利用鏡像站(如 hf-mirror.com):

$env:HF_ENDPOINT = "https://hf-mirror.com"
huggingface-cli download Qwen/Qwen-Image-2.1-Turbo --exclude "transformer/*" --local-dir models/Qwen-Image-2.1-Turbo

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *