华为昇腾服务器部署Qwen Image模型及推理服务

阿里的Qwen Image是通义千问推出的多模态图像生成模型系列,目前主要有两个重要版本:Qwen-Image(20B参数)和最新的Qwen-Image-2.0(7B参数)。

模型结构:Qwen Image采用多模态扩散变换器(MMDiT)架构,主要由三个核心组件构成:Qwen2.5-VL作为理解大脑负责语义理解,VAE(变分自编码器)负责图像压缩与重建,MMDiT作为主力生成器进行图像生成。最新版本Qwen-Image-2.0实现了文生图与图生图的二合一架构,支持1k token长指令输入和原生2K分辨率输出。

模型参数量:Qwen-Image原始版本拥有200亿(20B)参数,而2026年2月发布的Qwen-Image-2.0采用更轻量的7B参数设计,在保持高质量输出的同时显著提升了推理速度。

模型用途:该模型系列具备强大的多功能一体化能力:

  1. 图像生成与编辑:支持文生图、图生图、风格转换、物体增减、文本修改等精细编辑操作

  2. 复杂文本渲染:特别擅长中英文混合文本渲染,能准确生成包含多行文字、复杂排版的海报、PPT、信息图等专业内容

  3. 图像理解与分析:内置物体检测、语义分割、深度估计、文档解析等理解能力

  4. 应用场景:广泛用于广告设计、教育课件、社交媒体运营、工业设计、文化创意等领域

Qwen Image系列以其卓越的中文文本渲染能力和精准的图像编辑功能,在多个权威评测中表现优异,已成为全球AI社区备受关注的开源视觉基础模型。

目前我所在的企业也有部署Qwen Image模型的需求,主要是用于生成企业内部宣传的图片,或者是在图像识别中生成一些特定领域的异常事件的图片(用于给CV模型的训练提供样本),因此我将基于企业所用的华为昇腾的国产化算力来进行部署,并提供推理服务API给用户使用。

另外考虑到是提供给企业级的服务,需要确保不会被用户使用过来生成一些包含有害内容的图片,因此还需要引入一个对提示词进行安全检测的模型,确保不会有不安全的文生图提示词。这里我选用的是Qwen Guard模型,这是阿里通义千问团队于2025年9月推出的安全护栏模型系列,基于Qwen3架构构建,专门用于AI内容安全审查。该系列包含Qwen3Guard-Gen(生成式版)和Qwen3Guard-Stream(流式检测版)两大变体,提供0.6B、4B、8B三种参数规模,支持119种语言,采用安全、争议性、不安全三级风险分类体系,能够对用户输入和模型输出进行实时或离线的精准安全检测

Qwen Guard模型部署

采用VLLM Ascend推理框架来进行部署,下载模型权重后,通过以下命令进行加载,我采用的是910C算力卡。

bash 复制代码
docker run --rm \
    --name qwen-guard \
    --shm-size=16g \
    --net=host \
    --device /dev/davinci0 \
    --device /dev/davinci1 \
    --device /dev/davinci_manager \
    --device /dev/devmm_svm \
    --device /dev/hisi_hdc \
    -v /usr/local/dcmi:/usr/local/dcmi \
    -v /usr/local/Ascend/driver/tools/hccn_tool:/usr/local/Ascend/driver/tools/hccn_tool \
    -v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
    -v /usr/local/Ascend/driver/lib64:/usr/local/Ascend/driver/lib64 \
    -v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
    -v /etc/ascend_install.info:/etc/ascend_install.info \
    -v /data/Qwen3Guard-Gen-8B:/model \
    -itd  m.daocloud.io/quay.io/ascend/vllm-ascend:v0.12.0rc1-a3 bash

容器启动之后,进入容器,并输入以下命令启动vllm服务

bash 复制代码
nohup vllm serve /model --host 123.123.123.123 --port 8001 --served-model-name qwen-guard --tensor-parallel-size 2 & 

服务启动之后,我们可以调用接口进行测试,接口会返回对用户提示词是否安全的分类,总共有三个分类,分别是安全、有争议、不安全。其中有争议的分类表示还要结合之前的上下文来进行判断。

bash 复制代码
time curl -X POST "http://123.123.123.123:8001/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen-guard",
"messages": [
{"role": "user", "content": "test"}],
"stream": false,
"temperature": 0.01
}'

Qwen Image模型部署

部署Qwen Image需要适用VLLM Omni的镜像,或者基于VLLM镜像的基础上安装Omni的相关包。另外目前Qwen Image在910C的一个芯片上部署即可,而910C有两个芯片(各64G显存),为此我决定在1张910C卡上部署两个Qwen Image实例,通过Nginx来设立负载均衡连接到这两个实例。另外,考虑到文生图的时间可能会比较长,有可能通过企业级的网关访问时超时,原生的VLLM服务接口时要等图片完成生成之后才返回数据的,因此我还需要额外起一个API服务,当收到用户请求时每秒发送心跳信号,然后等图片数据完成之后分片传输。

Docker Compose启动服务

首先是编写一个docker-compose.yaml文件,把Qwen Image、Ngingx服务以及API服务都通过Docker Compose服务组合起来,代码如下。

bash 复制代码
version: '3.8'

# 定义所有服务都可能用到的通用配置,避免重复
x-common-vllm-config: &common-vllm-config
  image: vllm-omni:v1.0
  restart: unless-stopped
  shm_size: '16g'
  # 注意:使用 volumes 列表来挂载,而非 -v 参数
  volumes:
    - /usr/local/dcmi:/usr/local/dcmi
    - /usr/local/bin/npu-smi:/usr/local/bin/npu-smi
    - /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/
    - /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info
    - /etc/ascend_install.info:/etc/ascend_install.info
    - /Public_dir/Qwen-Image-2512:/model
  networks:
    - app-network
  # 容器启动后执行的命令
  command: bash -c "nohup vllm serve /model --served-model-name qwen_image --gpu-memory-utilization 0.8 --omni --port 8000 & sleep infinity"

x-common-devices-config: &common-devices-config
  devices:
    - /dev/davinci_manager
    - /dev/devmm_svm
    - /dev/hisi_hdc

services:
  # 定义具体的服务实例
  vllm-on-6:
    <<: *common-vllm-config
    container_name: vllm-on-device-6
    devices:
      - /dev/davinci_manager
      - /dev/devmm_svm
      - /dev/hisi_hdc
      - /dev/davinci6

  vllm-on-7:
    <<: *common-vllm-config
    container_name: vllm-on-device-7
    devices:
      - /dev/davinci_manager
      - /dev/devmm_svm
      - /dev/hisi_hdc
      - /dev/davinci7

  # 反向代理服务
  nginx-proxy:
    image: nginx:latest
    container_name: nginx-reverse-proxy
    ports:
      - "4200:4200" # 将代理服务的端口映射到宿主机
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro # 挂载自定义的Nginx配置文件
    depends_on:
      - vllm-on-6
      - vllm-on-7
    networks:
      - app-network
    restart: unless-stopped

  api-server:
    image: ubuntu22.04_python3.13:v1.0
    container_name: image_generate_api
    volumes:
      - ./app:/app
    environment:
      - safeguard_api=http://123.123.123.123:4100/v1
      - model=qwen-image
      - PATH=/root/miniconda3/bin:${PATH}
    entrypoint: ["python", "/app/server.py"]
    depends_on:
      - nginx-proxy
    networks:
      - app-network

networks:
  app-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.30.0.0/16
          gateway: 172.30.0.1

在以上的Docker Compose文件中,定义了启动两个VLLM Omni的Qwen Image实例,一个Nginx反向代理服务,还有一个提供对外访问接口的api-server。

Nginx配置

nginx.conf的配置如下,这里定义了两个url, 其中/api/是提供给api-server访问后端的vllm服务的,/generate是api-server暴露给外部用户访问的服务接口。这里还对服务的访问进行了限流。

bash 复制代码
events {
    worker_connections 1024;
}

http {
    upstream vllm_backends {
        # 指向 Docker Compose 网络中的服务名和容器内部端口
        server vllm-on-6:8000;
        server vllm-on-7:8000;
        # 可以配置负载均衡策略,如 least_conn; (最少连接) 或 ip_hash; (会话保持)
    }

    upstream api_backends {
        server api-server:8000;
    }

    limit_req_zone $server_name zone=api_per_minute:10m rate=5r/m;
    limit_conn_zone $server_name zone=perserver:10m;

    server {
        listen 4200;

        location /api/ {
            allow 172.30.0.0/16;
            allow 127.0.0.1;
            deny all;

            # 将请求代理到上游 vLLM 服务器组
            proxy_pass http://vllm_backends/;
            # 以下设置确保正确传递客户端信息给后端[2](@ref)
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            # 可选:设置与后端服务器通信的超时时间
            proxy_connect_timeout 300s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;
        }

        location /generate {
            limit_conn perserver 2;
            limit_req zone=api_per_minute burst=3 nodelay;

            proxy_buffering off;
            proxy_request_buffering off;

            proxy_pass http://api_backends/generate-image;

            proxy_connect_timeout 60s;
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;

            proxy_http_version 1.1;
            proxy_set_header Connection "";

            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            limit_req_status 503;
            limit_conn_status 503;
        }

    }
}

API服务编写

下面用fastapi来编写一个服务,实现当用户调用时,对用户的文生图提示词进行安全检测,然后在图像生成未完成之前,每秒发送心跳信号,最后在图片生成之后通过分块的方式返回给用户。代码如下。

python 复制代码
from openai import AsyncOpenAI, OpenAI
import base64
import re
from io import BytesIO
import asyncio
import time
import json
import os

from fastapi import FastAPI, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
import uvicorn
from pydantic import BaseModel

app = FastAPI(title="文生图流式服务")
safeguard_model_url = os.environ.get("safeguard_api")
safeguard_model_name = "qwen-guard"
client_safeguard = OpenAI(base_url=safeguard_model_url, api_key="abc")
imagetotext_model_url = "http://nginx-proxy:4200/api/v1"
imagetotext_model_name = os.environ.get("model")
client_imagetotext = AsyncOpenAI(base_url=imagetotext_model_url, api_key="abc")

safe_pattern = re.compile(r'Safety: (.*)\nCategories')
image_pattern = re.compile(r'data:image/(?P<ext>[a-zA-Z]+);base64,(?P<data>.+)')

# 添加CORS中间件以支持跨域请求
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

class ImageRequest(BaseModel):
    prompt: str
    width: int = 1024
    height: int = 1024
    steps: int = 20
    negative_prompt: str = ''

def check_prompt_safety(prompt: str):
    response = client_safeguard.chat.completions.create(
        model=safeguard_model_name,
        messages=[
            {
                "role": "user",
                "content": prompt
            }
        ]
    )
    content = response.choices[0].message.content
    print(content)
    match = re.match(safe_pattern, content)
    if match:
        return match.group(1)
    else:
        return 'Unsafe'

async def generate_image(prompt: str, width: int, height: int, num_inference_steps: int, negative_prompt: str):
    """
    调用文生图大模型,获取图片的base64编码
    """
 
    response = await client_imagetotext.chat.completions.create(
        model=imagetotext_model_name,
        messages=[
            {
                "role": "user",
                "content": prompt
            }
        ],
        extra_body={"size": f"{width}x{height}", "num_inference_steps": num_inference_steps, "negative_prompt": negative_prompt}
    )            
        
    data_url = response.choices[0].message.content[0]['image_url']['url']
    match = re.match(image_pattern, data_url)

    if match:
        base64_data = match.group('data')
        return base64_data
    else:
        return "Error to generate image"

@app.post("/generate-image")
async def generate_image_stream(request: ImageRequest, background_tasks: BackgroundTasks):
    """
    流式文生图接口
    - 先发送心跳包保持连接
    - 图片数据生成后分块传输
    """
    async def event_stream(safety_flag: bool):
        image_generated = False
        image_data = None
        if safety_flag:
            # 启动后台任务生成图片
            async def generate_image_task():
                nonlocal image_data, image_generated
                image_data = await generate_image(request.prompt, request.width, request.height, request.steps, request.negative_prompt)
                image_generated = True
            
            # 在后台运行图片生成任务
            asyncio.create_task(generate_image_task())
            start_time = time.time()
            # 第一阶段:发送心跳包保持连接
            print(f"First, {image_generated}")
            while not image_generated and (time.time() - start_time) < 600:  # 超时保护
                elapsed = time.time() - start_time
                status_data = {
                    "type": "status",
                    "elapsed": round(elapsed, 2),
                    "status": "processing",
                    "message": "图片生成中..."
                }
                yield f"data: {json.dumps(status_data, ensure_ascii=False)}\n\n"
                await asyncio.sleep(1)
            
            # 第二阶段:传输图片数据
            if image_data:
                # 发送开始传输的信号
                start_data = {
                    "type": "start_transfer",
                    "message": "开始传输图片数据",
                    "total_size": len(image_data)
                }
                yield f"data: {json.dumps(start_data, ensure_ascii=False)}\n\n"
                
                # 分块传输图片数据
                chunk_size = 4096  # 4KB每块
                total_chunks = (len(image_data) + chunk_size - 1) // chunk_size
                
                for i in range(total_chunks):
                    start_idx = i * chunk_size
                    end_idx = min((i + 1) * chunk_size, len(image_data))
                    chunk = image_data[start_idx:end_idx]
                    
                    chunk_data = {
                        "type": "image_chunk",
                        "chunk_index": i,
                        "total_chunks": total_chunks,
                        "chunk_size": len(chunk),
                        "data": chunk
                    }
                    yield f"data: {json.dumps(chunk_data)}\n\n"
                    await asyncio.sleep(0.01)  # 小延迟避免过快传输
                
                # 发送结束信号
                end_data = {
                    "type": "complete",
                    "message": "图片传输完成",
                    "total_chunks": total_chunks,
                    "total_size": len(image_data)
                }
                yield f"data: {json.dumps(end_data, ensure_ascii=False)}\n\n"
            else:
                # 图片生成失败
                error_data = {
                    "type": "error",
                    "message": "图片生成超时或失败"
                }
                yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"
        else:
            # 提示词不合规
            error_data = {
                "type": "error",
                "message": "提示词不合规"
            }
            yield f"data: {json.dumps(error_data, ensure_ascii=False)}\n\n"

    safety_result = check_prompt_safety(request.prompt)
    safety_flag = False if safety_result=='Unsafe' else True
    return StreamingResponse(
        event_stream(safety_flag),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "Access-Control-Allow-Origin": "*",
        }
    )

if __name__ == "__main__":
    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8000,
        log_level="info"
    )

调用文生图服务测试

全部完成后,在目录下通过docker compose up来启动服务。然后我们可以通过以下的一个客户端程序来调用,测试是否能正常提供服务。

python 复制代码
import requests
import json
import time
import base64

def test_image_generation():
    """测试文生图流式接口"""
    url = "http://123.123.123.123:4200/generate"
    
    # 准备请求数据
    request_data = {
        "prompt": "一只在星空下飞翔的龙,奇幻风格,细节丰富",
        "width": 1024,
        "height": 1024,
        "steps": 20
    }
    
    print("开始请求文生图服务...")
    print(f"提示词: {request_data['prompt']}")
    print("-" * 50)

    headers = {
        "Content-Type": "application/json"
    }
    print(headers)
    try:
        response = requests.post(url, json=request_data, stream=True, timeout=60, headers=headers, verify=False)
        response.raise_for_status()
        
        image_chunks = []
        start_time = time.time()
        
        for line in response.iter_lines(decode_unicode=True):
            if line.startswith('data: '):
                data_str = line[6:]  # 去掉 'data: ' 前缀
                if data_str.strip():
                    try:
                        data = json.loads(data_str)
                        elapsed = time.time() - start_time
                        
                        if data["type"] == "heartbeat":
                            print(f"[{elapsed:.2f}s] 心跳: {data['message']}")
                            
                        elif data["type"] == "status":
                            print(f"[{elapsed:.2f}s] 状态: {data['message']}")
                            
                        elif data["type"] == "start_transfer":
                            print(f"[{elapsed:.2f}s] 开始传输图片,总大小: {data['total_size']} 字节")
                            
                        elif data["type"] == "image_chunk":
                            print(f"[{elapsed:.2f}s] 接收块 {data['chunk_index']+1}/{data['total_chunks']}")
                            image_chunks.append(data["data"])
                            
                        elif data["type"] == "complete":
                            print(f"[{elapsed:.2f}s] 传输完成! 总块数: {data['total_chunks']}")
                            
                            # 重组图片
                            full_image_data = "".join(image_chunks)
                            
                            # 保存图片
                            import base64
                            image_bytes = base64.b64decode(full_image_data)
                            with open("generated_image.png", "wb") as f:
                                f.write(image_bytes)
                            
                            print(f"图片已保存为 generated_image.png")
                            break
                            
                        elif data["type"] == "error":
                            print(f"[{elapsed:.2f}s] 错误: {data['message']}")
                            break
                            
                    except json.JSONDecodeError as e:
                        print(f"JSON解析错误: {e}")
                        
    except requests.exceptions.Timeout:
        print("请求超时")
    except requests.exceptions.RequestException as e:
        print(f"请求错误: {e}")
        print(response.text)

if __name__ == "__main__":
    test_image_generation()

运行以上程序,可以看到可以正确的生成图片。

相关推荐
AC赳赳老秦1 小时前
边缘AI落地趋势:DeepSeek在工业边缘节点的部署与低功耗优化技巧
人工智能·python·算法·云原生·架构·pygame·deepseek
模型时代1 小时前
诺基亚预测广域网流量大幅增长但专家质疑假设
人工智能
yhdata1 小时前
锁定294.2亿元!2032年物理入侵检测系统市场规模前瞻,产业布局加速推进
人工智能
小鸡吃米…1 小时前
TensorFlow 实现线性回归
人工智能·python·tensorflow·线性回归
星爷AG I2 小时前
12-1 社会认知(AGI理论基础)
人工智能
callJJ2 小时前
Java 源码阅读方法论:从入门到实战
java·开发语言·人工智能·spring·ioc·源码阅读
予枫的编程笔记2 小时前
【Docker进阶篇】Docker Compose 实战:一键启动Web+数据库+缓存,微服务环境部署不再绕弯
人工智能·docker·开发效率工具·容器编排·docker compose·后端开发·微服务部署
蚂蚁数据AntData2 小时前
DB-GPT 0.7.5 版本更新:基于 Falcon 评测集的Text2SQL评测体系全面升级,支持LLM/Agent两种评测模式和多环境评测
大数据·人工智能·算法·ai·开源