背景痛点:传统 UI 框架为何“跑不动”大模型
第一次把 7B 参数的 LLM 塞进 Gradio 时,我整个人是懵的:
- 每点一次“Generate”,浏览器转圈 3 秒才出字,GPU 占用却直接飙到 95%。
- 多开两个标签页,显存 OOM,直接炸掉。
- 状态管理全靠
st.session_state,调试时打印一堆字典,越打越乱。
问题根源并不在模型,而在 UI 层:
- 同步阻塞式推理:前端一次请求,后端一次推理,整个 Python 进程被卡住。
- 全量重渲染:Gradio/Streamlit 每次交互把整张页面重新吐给浏览器,DOM 越大越慢。
- 无差别显存拷贝:模型权重、KV-Cache、临时张量全部往显存里堆,用完也不主动释放。
一句话:传统 UI 把大模型当成“普通函数”调用,却忽略了它其实是“吃显存、吃算力、吃异步”的三高怪兽。
技术对比:把 ComfyUI 拉来“打擂台”
我选了同一台 3080Ti、同一版 Llama-7B-chat,用三种框架跑“连续 100 次 512 token 续写”压测,结果如下:
| 维度 | ComfyUI | Gradio 3.28 | Streamlit 1.24 |
|---|---|---|---|
| 首 token 延迟 | 0.18 s | 2.3 s | 2.1 s |
| 100 次总耗时 | 31 s | 210 s | 198 s |
| 峰值内存 | 6.8 GB | 10.2 GB | 9.7 GB |
| 并发 5 用户 | 无 OOM | 第 3 用户 OOM | 第 4 用户 OOM |
| 扩展方式 | 拖节点即可 | 写 Python 回调 | 写 Python 回调 |
| 前端增量更新 | 是 | 否 | 否 |
结论:ComfyUI 把“异步+增量+零拷贝”做成默认,省掉 80% 以上无谓开销,对新手最友好——前提是你得先把它跑起来。
核心实现:15 分钟搭一个可复用 LLM 节点
1. 异步消息总线长啥样
ComfyUI 的“心脏”是一条轻量级消息总线:
- 前端用 WebSocket 订阅
/ws - 后端用
asyncio.Queue把“节点输出”推给前端 - 每个节点只关心“输入-计算-输出”,天然解耦
2. 封装 LLM 推理节点(可直接复制跑)
# llm_node.py import torch from typing import Tuple from comfyui.nodes.base import BaseNode # 官方基类 from transformers import AutoTokenizer, AutoModelForCausalLM class LLMGenerate(BaseNode): # 节点元数据 CATEGORY = "llm" RETURN_TYPES = ("STRING",) FUNCTION = "generate" @classmethod def INPUT_TYPES(cls): return { "required": { "prompt": ("STRING", {"multiline": True}), "max_new_tokens": ("INT", {"default": 128, "min": 1, "max": 2048}), "temperature": ("FLOAT", {"default": 0.7, "min": 0.1, "max": 2.0}), } } def __init__(self): # 模型只加载一次,全局复用 self.tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf") self.model = AutoModelForCausalLM.from_pretrained( "meta-llama/Llama-2-7b-chat-hf", torch_dtype=torch.float16, device_map="auto" ) self.model.eval() # 推理模式,关闭 dropout def generate(self, prompt: str, max_new_tokens: int, temperature: float) -> Tuple[str]: try: inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device) with torch.no_grad(): outputs = self.model.generate( **inputs, max_new_tokens=max_new_tokens, temperature=temperature, do_sample=True, pad_token_id=self.tokenizer.eos_token_id ) # 只解码新增部分,减少 IO new_tokens = outputs[0][inputs.input_ids.shape[1]:] result = self.tokenizer.decode(new_tokens, skip_special_tokens=True) return (result,) except RuntimeError as e: # 显存不足 # NOTE: 把异常包装成前端可读的字符串,避免整条流程崩溃 return (f"[GPU OOM] {e}",)要点:
- 类型检查交给 ComfyUI 的
INPUT_TYPES声明,前端自动出表单。 - 异常被“吃掉”后转成字符串,节点永不崩溃,整条 Workflow 继续跑。
- 模型权重
self.model只初始化一次,后续调用纯显存计算,零拷贝。
生产考量:让老板敢签字上线
1. 压力测试脚本(locust)
# locustfile.py from locust import HttpUser, task, between class ComfyUIUser(HttpUser): wait_time = between(1, 2) host = "http://127.0.0.1:8188" @task def run_llm_workflow(self): # 先调用 /prompt 提交 workflow json,再轮询 /history payload = { "prompt": { "1": {"inputs": {"prompt": "写一段 Python 快排"}, "class_type": "LLMGenerate"}, "2": {"inputs": {"text": ["1", 0]}, "class_type": "SaveText"} } } with self.client.post("/prompt", json=payload, catch_response=True) as resp: if resp.status_code != 200: resp.failure("submit failed")跑 5 分钟就能画出 RPS-延迟曲线,显存占用一目了然。
2. 内存泄漏检测(tracemalloc)
# trace_leak.py import tracemalloc, time, asyncio from comfyui.server import run_server tracemalloc.start(25) # 保留 25 帧 async def monitor(): while True: await asyncio.sleep(30) current, peak = tracemalloc.get_traced_memory() snapshot = tracemalloc.take_snapshot() top = snapshot.statistics("lineno")[:10] print("=== 30s 内存快照 ===") for t in top: print(t) if __name__ == "__main__": asyncio.create_task(monitor()) run_server()把 top 统计打到日志里,连续跑 24h,若current持续单向上涨,就能定位到具体行号。
避坑指南:三条黄金法则
节点无共享可变状态
所有跨节点数据走“端口”——也就是消息总线。千万别在全局字典里塞临时张量,否则并发时互相覆盖,调试到哭。先 clone 再切片
给下游节点传张量时,默认传引用。如果下游会 in-place 修改,一定tensor.clone(),否则上游结果会被污染。模型热加载顺序
- 先在
__init__里torch.cuda.empty_cache() - 再
load_state_dict(..., strict=False) - 最后
model.to(device)
顺序反了,显存碎片会多 20%。
- 先在
思考题:动态节点编排怎么玩?
静态 Workflow 拖来拖去很爽,但业务场景经常“按用户等级自动选择 4B/7B/13B 模型”,节点图得在运行时拼出来。
提示:ComfyUI 的/prompt接口接受纯 JSON,你可以前端可视化编辑器里只画“模板”,真正 prompt 里用代码把子图拼接进去。
不妨动手试试:
- 用 Vue-Flow 画一个空画布
- 让用户点击“添加 LLM 节点”
- 前端实时生成 JSON,调
/prompt运行
把成品贴到评论区,一起交流!
写完这篇,我把公司内部的“文案生成”服务从 Streamlit 迁到 ComfyUI,同样 4 卡 A10,并发能力翻了 6 倍,显存还降了 1.8 GB。
ComfyUI 不是银弹,但把“异步、节点化、零拷贝”做成默认后,新手也能一天内搭出可上线的 LLM 交互界面。剩下的,就是不断压测、监控、调优——老板要的“稳定”二字,也就稳了。