这篇文章将会介绍一套Stable Diffusion API服务的部署方案,能够支持服务的自动扩缩容,从而支撑多人在线请求;这套部署方案不需要手动修改或运行任何代码,只需要照着操作步骤通过网页操作即可;同样支持自定义模型和Lora,支持webui插件比如ControlNet,并且支持上传任意多的模型以及切换模型,上传模型后无需重新构建镜像,立即就能使用;同时,这套方案所使用的云平台也十分经济。
接下来开始详细介绍方案的部署步骤。
这次部署方案用到的是runpod平台,它可以支持我们自定义扩缩容的策略,使得我们可以选择对于我们的场景最经济的策略;同时费用也十分经济,只针对GPU使用时长收费,缩容之后就不会计费了,最低配的A4000 GPU每秒钟$0.0002
部署前,我们只需要准备好runpod平台的账号,以及我们自己想要部署的模型就行了。
一、将模型上传到数据盘
首先来到runpod的数据盘操作平台,我们需要新建一个数据盘:www.runpod.io/console/use… Volume”新建:
“Volume Name”随便取名就行,存储空间可以根据自己打算部署的模型文件的总大小来大概估算一下,这个之后也可以进行修改。之后点击“Create”就行。数据盘的费用是每GB每个月$0.07
创建好数据盘之后我们需要将模型上传到数据盘,点击左侧的“Secure Cloud”,然后在右边选择自己刚刚创建的数据盘名字,之后在下方可用的GPU型号中,选择一个型号点击“Deploy”
部署的时候记得勾选“Start Jupyter Notebook”,之后等待部署完成。部署好之后就能看到自己的机器了,点击“Connect”:
在弹出的窗口中点击“Connect to Jupyter Lab”(如果不需要使用图形界面,也可以直接用SSH的方式连接进去下载模型,这样下载速度会快很多):
之后就会打开一个Jupyter Notebook的页面,我们就在这里进行模型上传的操作:
我们在/workspace 目录下新建一个models文件夹,然后在里面按照Stable-diffusion-webui的目录结构把文件都放好就行:
比如说我们想要部署的checkpoint,就放在”Stable-diffusion”文件夹里面上传checkpoint就行了,也可以上传多个:
上传完成之后,我们就不需要部署的这台机器了,可以回收掉以免一直计费。
二、部署镜像
来到runpod serverless平台:www.runpod.io/console/ser… Templates”,之后点击“New Template”:
在弹出的窗口里创建一个Template:
Template Name随便取一个就行,我这里叫“sd-demo”,“Container Image”这里需要填写我这次专门构建的镜像名;Container Disk填写15GB就够用了,因为我们的模型是不需要放在里面的。
填写好之后点击“Save Template”保存。接下来点击“My Endpoints”,然后点击“New Endpoints”,在弹出的窗口里填写下面的配置:
- “Select Template”这里选择我们刚刚创建的Template
- “Select Network Volume”选择我们第一步中上传了模型的数据盘
- “FlashBoot”选项可以加快扩容时候的拉取镜像的速度,可以勾选上
- “Min Workers”:当一段时间没有请求时,机器会回收以节省我们的成本。这里可以指定最少要保留的机器数量
- “Max Workers”:当请求数过多时,会进行扩容,这里可以指定最大扩容到多少台,防止我们成本爆炸
- “Idle Timeout”:缩容策略,当多少秒之内没有请求,就进行缩容,可以根据实际场景进行指定
- “Scale Type”:扩容策略,如果选择“Queue Delay”,就是根据处理请求的延时进行扩容,当延时超过多少秒时则扩容新的机器;如果选择“Request Count”,就是根据排队中的请求数量进行扩容。
之后点击“Create”创建。创建之后,每台机器都需要拉取镜像,然后进行模型的初始化,我们也可以点击“logs”查看部署日志,初始化完成之后,我们应该能够看到模型加载成功的日志:
三、调用模型
现在我们的服务已经是可用的状态了,可以使用我们之前上传的模型进行文生图、图生图、切换模型的操作。接下来我会以文生图为例讲解如何调用API。
首先需要去runpod的“Settings — API Keys”里面创建一个API key。接下来我们看一下服务的API接口:
可以注意到我们的Endpoint的API是专属的。网页版上面提供的几个接口,我们主要用到的是:
- RUN:这个是调用模型的异步接口,也就是调用之后会立刻返回一个任务id,之后需要用这个任务id去轮询STATUS接口,获取任务执行情况;
- STATUS:根据任务id获取任务执行情况的接口,如果任务执行完成,则会返回任务执行的结果;
- CANCEL:根据任务id,取消任务的执行
我们在本地用python写一个简单的发起任务并轮询任务结果的代码:
import requests
import time
import json
import base64
PROMPT_CHILL_DEFAULT = '''
(8k, RAW photo, best quality, masterpiece:1.2), (realistic, photo-realistic:1.37),<lora:koreanDollLikeness_v10:0.5> <lora:stLouisLuxuriousWheels_v1:1>,st. louis (luxurious wheels) ,1girl,(Kpop idol), (aegyo sal:1),hair ornament, portrait, necklace,cute, night, professional lighting, photon mapping, radiosity, physically-based rendering, thighhighs, smile, pose, silver hair,sheer sleeveless white shirt, white skirt, white pantyhose,cat ear,room, iso 950, HDR+, white balance
'''
# 需要替换为自己的Endpoint ID和runpod API key
endpoint = "53uzy6jz3sv5uq"
RUNPOD_TOKEN = ""
def chill_watcher_generate(prompt: str):
# 调用run接口异步发起任务
url = "https://api.runpod.ai/v2/{}/run".format(endpoint)
form_data = {
"input": {
"api": "txt2img",
"prompt": prompt,
# model填写想切换的模型,也可以置空
"model": "chilloutmix_NiPrunedFp32Fix.safetensors"
}
}
header = {
"Content-Type": "application/json",
"Authorization": "Bearer "+RUNPOD_TOKEN,
}
resp = requests.post(url, headers=header, json=form_data)
if resp.status_code != 200:
print("chill watcher error: ", resp.content)
return ""
resp_data = json.loads(resp.content)
print(resp_data)
gen_id = resp_data["id"]
# 开始轮询任务状态
while True:
status = get_status(gen_id)
status_json = json.loads(status)
if status_json.get("executionTime", "") != "":
print("job finished, cost(milli seconds): ", status_json["executionTime"])
if status_json.get("output", "") != "":
if status_json["output"].get("err", "") != "" or status_json["output"].get("msg", "") != "":
print("get error: ", status_json["output"])
return ""
print("generate params: ", status_json["output"]["parameters"])
img_b64 = status_json["output"]["img_data"]
img_data = base64.b64decode(img_b64)
with open("1.png", 'wb') as f:
f.write(img_data)
return ""
else:
print(status)
time.sleep(1.5)
def get_status(id: str):
# 获取任务状态
url = "https://api.runpod.ai/v2/{}/status/".format(endpoint) + id
header = {
"Authorization": "Bearer "+RUNPOD_TOKEN,
}
resp = requests.post(url, headers=header)
if resp.status_code != 200:
print("chill watcher error: ", resp.content)
return ""
return resp.content
chill_watcher_generate(PROMPT_CHILL_DEFAULT)
其中第17行的“input”部分,就是文生图API实际支持的输入参数。API接口介绍如下(文生图、图生图、查看可用模型):
# 文生图
request:
- api # 必传“txt2img”
- model # 模型名称,如“chilloutmix_NiPrunedFp32Fix.safetensors”,也可以置空
- prompt
- negative_prompt
- sampler_name # 如“DPM++ SDE Karras”
- steps # 默认20
- cfg_scale # 默认8
- width # 默认512
- height # 默认768
- seed # 不传则使用随机种子
- restore_faces # 布尔值
- hires_fix # 布尔值,是否启用hires.fix
- hr_denoising # 默认0.7
- hr_scale # 默认2.0
- hr_resize_x
- hr_resize_y
- hr_steps # 默认10
- hr_upscaler
response:
- img_data # 生成的图片的base64编码
- parameters # 本次生成的数据,包括seed、模型名称等等
# 图生图
request:
- api # 必传“img2img”
- model # 模型名称,如“chilloutmix_NiPrunedFp32Fix.safetensors”,也可以置空
- prompt
- negative_prompt
- sampler_name # 如“DPM++ SDE Karras”
- steps # 默认20
- cfg_scale # 默认8
- width # 默认512
- height # 默认768
- seed # 不传则使用随机种子
- restore_faces # 布尔值
- resize_mode # 默认0
- denoising_strength # 默认0.75
- init_images # 一个list,list里面的元素是输入图像的base64编码
response:
- img_data # 生成的图片的base64编码
- parameters # 本次生成的数据,包括seed、模型名称等等
# 列出可用模型
request:
- api # 必传“list_models”
response:
- models # 模型名称列表
调用之后,我们也能在网页上看到生成的日志:
除了本文部署所使用的包含文生图、图生图API的镜像之外,我还构建了另外一个镜像,包含了目前sd-webui的所有API(调用方式、输入输出参数和webui的API完全相同),并且也支持ControNet插件的API,大家可以参考这篇文章