几年前,当我终于能够使用人工智能图像生成工具时,我兴奋极了,立刻坐下来写了一篇关于它的文章(使用Node.js和OpenAI的DALL-E)。将想法直接转化为数字像素这种神奇的过程,感觉就像握着一把真正的魔法杖一样。

但那时,使用这些工具并不容易。我们主要可以选择的是Midjourney,不过这意味着你需要在Discord上花费大量时间进行操作,而且有时会因为流量限制或服务器过于繁忙而无法使用这些工具。

在当时,想要使用图像生成工具,简直就像在人群拥挤的地方尝试点咖啡一样困难。

幸运的是,现在情况已经完全改变了。如今,我们不仅可以在普通的消费级硬件上运行像Stable Diffusion这样的先进模型,还可以在本地、离线环境下免费使用它们。我们不需要任何API密钥,也没有订阅流量限制,更不需要处理Discord频道之类的问题。

在这个教程中,我们将使用Node.js、Express、Socket.io以及QVAC SDK来构建一个本地Web应用程序,从而运行量化的Stable Diffusion 2.1模型。

目录

先决条件

要想充分掌握这个教程的内容,你需要具备以下基础知识:

  • Node.js和ES模块:对现代JavaScript模块系统(import/export)、异步循环以及事件监听器有基本的了解。

  • Express和WebSockets:熟悉如何使用socket.io来路由静态文件并通过WebSockets发送实时消息。

  • HTML和纯CSS:理解基本的DOM操作和样式设置方法。

  • 开发环境

    :一台安装了Node.js的本地计算机。

什么是QVAC?

由Tether开发的QVAC是一系列专为在客户端硬件上直接运行机器学习模型而设计的工具。

与将推理请求转发到成本较高的云托管API(如DALL-E或Midjourney)不同,QVAC直接将预先编译好的机器学习运行时程序(例如用于文本处理的llama.cpp、用于转录的whisper.cpp,以及自定义的扩散模型后端)集成到Node.js、移动端和桌面应用程序中。

使用QVAC运行本地的AI模型具有以下几个实际优势:

  • 零API费用:只要您的硬件能够处理,就可以生成任意数量的图像,而无需支付任何重复费用。

  • 隐私优先:提示信息及生成的图像都会完全保存在您本地机器的内存中。

  • 离线使用不受限制:您可以在隔离的网络环境中、在飞行途中,或在没有互联网连接的地区运行该应用程序。

Stable Diffusion的内部工作原理

为了能够在不耗尽RAM的情况下本地生成图像,QVAC采用了量化的Stable Diffusion 2.1 GGUF模型(代码为SD_V2_1_1B_Q8_0)。

但这种图像生成过程在概念上究竟是如何运作的呢?需要明确一点:这并不是一篇科学论文。我们不会深入探讨多元微积分、概率分布或随机微分方程等内容,因为我也不是从事底层机器学习研究的专家(说实话,我们谁也不想在屏幕上看到那些希腊符号和线性代数公式,而宁愿编写简洁明了的JavaScript代码)。

相反,我们会用一些直观的开发者类比来理解这些模型的工作原理。

世界级雕塑家的类比

从本质上讲,现代AI图像生成技术就是将随机性转化为现实。与人类插画师用画笔逐像素“绘制”图像不同,AI更像是一位世界级的雕塑家,它从一块数字化的静态数据中雕刻出图像。

目前推动这一技术发展的核心技术是扩散算法,许多模型如Stable Diffusion、Midjourney以及谷歌的Imagen系列都是基于这种技术开发的。

以下是这块静态数据如何逐步转化为艺术作品的详细过程:

1. 训练阶段(学习模式)

在模型能够生成任何图像之前,它必须先分析数十亿张图像及其对应的文字描述。在这个阶段,开发者会采取一些看似反常的做法:他们故意破坏这些图像

  • 添加噪声:系统会选取一张清晰的图片(例如一只猫的图片),然后逐像素地添加随机数字噪声,直到原始图像完全无法被识别为止。

  • 学习如何消除这些噪声:AI的任务就是分析这些被加了噪声的图像,并准确判断在哪个步骤中添加了多少噪声。通过反复进行这样的训练,AI就能掌握去除噪声的技术——也就是将混乱重新转化为有序的状态。

2. 将文字与视觉图像关联起来(CLIP技术)

<为了确保人工智能能够理解“一只戴着高顶礼帽的猫”长什么样子,它会使用一种文本到图像的转换机制,而这种机制通常是由名为CLIP(对比语言-图像预训练系统)的技术来实现的。

  • CLIP会将人类语言转化为一种数学映射结构(称为嵌入向量)。

  • 在这种映射中,“猫”这个词与实际表示猫的像素会位于非常接近的位置。这样,当你输入一个提示语时,AI就能准确知道应该从内存中提取哪些视觉信息。

3. 生成阶段(反向扩散循环)

当你输入提示语并点击“生成”按钮时,整个过程会以相反的顺序进行:

  • 初始状态:AI会从一片由完全随机的数字噪声构成的“画布”开始处理。

  • 提示语的引导作用:AI会根据你输入的提示语,利用其文本嵌入向量来指导后续的计算过程。它会在这片随机噪声中寻找与“猫”相关的特征。

  • 逐步去除噪声:AI会逐层去除噪声,使图像逐渐变得清晰。这个过程会重复20到50次。经过这些步骤后,模糊的形状会变成清晰的轮廓,纹理也会显现出来,最终就会生成一张清晰、高质量的新图像。

关于“种子值”的一个小知识:由于整个过程每次都是从完全随机的噪声开始的,因此即使两次输入完全相同的提示语,生成的图像也会不同(除非你使用特定的“种子值”来锁定初始的随机状态)。

下面这张图展示了利用扩散模型进行去噪的过程:

利用扩散模型进行去噪的示意图

潜在扩散技术:提升处理速度(变分自编码器的作用)

要逐像素生成高分辨率图像,就需要强大的计算能力。如果我们在消费级硬件上直接进行这样的操作,计算机很快就会过热,而且整个生成过程也需要花费数小时的时间。

为了解决这个问题,现代模型采用了潜在扩散技术

模型并不直接处理原始大小的图像,而是先使用一个称为编码器的组件将图像压缩到一个规模较小的抽象数学空间中(也就是“潜在空间”)。可以把这个空间想象成一个缩小了的“游乐场”,所有的去噪计算都在这里进行。由于这个空间的规模很小,因此计算速度会快很多。

当潜在空间中的去噪过程完成后,另一个称为解码器的组件会将这些数据重新还原成清晰的高分辨率图像,供我们查看。

QVAC支持的架构类型

当你使用QVAC进行本地推理时,SDK会连接到经过优化、由社区维护的C++后端程序。QVAC负责管理不同AI模型在硬件上的适配过程以及模型的生命周期管理。

  1. 文本生成(llama.cpp): 用于像Llama 3或Mistral这样的大型语言模型,实现自回归式的字符预测功能。

  2. 音频转录(whisper.cpp): 用于高效地进行语音转文本操作。

  3. 图像生成(stable-diffusion.cpp/sdcpp-generation): 本教程的重点内容。根据所选择的模型架构,QVAC提供了两种不同的图像生成方法:

    • 集成模型法(Stable Diffusion 1.5/2.1/XL): 这是一种传统的构建方式,整个处理流程(文本编码器、VAE模型以及差分扩散网络)被整合到一个统一的GGUF文件中(例如SD_V2_1_1B_Q8_0)。

      这种方式非常适合本地部署,因为只需加载一个文件即可开始生成图像。

    • 模块化多模型法(Flux):FLUX.1这样的现代架构采用了更为复杂的设计。Flux将计算组件分解为多个独立的模块:你需要加载核心的差分扩散变换器模型,同时还需要单独加载大型文本编码器(如T5-v1.1-xxl和CLIP-L)以及VAE模型。

      虽然这种方式需要同时加载多个文件,但由于使用了专门设计的文本理解模型,因此在生成图像的质量和准确性方面表现更为出色。

  4. 语音合成(TTS): 采用专门的架构来实现语音合成功能,例如Chatterbox(基于Transformer的零样本语音克隆技术)和Supertonic(基于差分扩散技术的噪声去除算法)。

GPU限制:Metal、AMD以及Intel Mac带来的问题

当在Apple Mac硬件上本地运行机器学习模型时,QVAC会尝试通过为Metal API编译计算流程来利用系统的GPU加速执行过程。

如果你使用的是Apple Silicon Mac(M1、M2、M3、M4或M5芯片),这一机制可以无缝工作,生成图像的过程会在几秒钟内完成,因为这些操作会利用Apple Neural Engine和统一的GPU内存。

但如果你使用的是配备独立AMD Radeon GPU的旧款Intel Mac电脑(比如16英寸MacBook Pro中常见的AMD Radeon Pro 5500M),就会遇到严重的驱动程序级限制:

  • 针对较旧的AMD独立GPU,macOS的Metal驱动程序不支持stable-diffusion.cpp所使用的现代机器学习计算着色器及矩阵运算功能。

  • 当推理进程尝试执行这些不受支持的操作时,驱动程序会无法编译相应的计算流程,从而导致严重的C++崩溃(错误代码为),进而使后台工作进程突然终止。

如果遇到这种硬件限制,那么每次触发图像生成操作时,默认的GPU配置都会导致应用程序崩溃。

device设置为"cpu",从而让模型在CPU上运行;同时还需要指定线程数(例如threads: 4)。虽然在CPU上生成图像所需的时间比在GPU上要长,但任何机器都能正常运行该程序,无论其GPU的性能如何。

图像生成流程

为了协调各个步骤的执行顺序,我们的应用程序建立了一个实时事件处理流程:

[浏览器客户端]                                  [Node.js服务器]
       |                                                 |
       | ------ 1. 连接并加载模型 --------->    |
       | <----- 2. 下载并加载模型 ----------     | (模型已缓存在本地)
       |                                                 |
       | ------ 3> 输入提示信息("舒适的小屋..." ) -->  |
       |                                                 |
       |                                                 | === [QVAC推理引擎] ===
       |                                                 | 
       | <----- 4> 执行去噪处理步骤(例如第5/20步)-- | (各处理步骤均实时进行)
       |                                                 |
       | <----- 5> 发送最终生成的图像(以Base64格式传输) --> | (数据直接存储在内存中)
       |                                                 |

完整实现过程

让我们来看看具体的实现步骤。你可以克隆整个项目代码库来跟随学习,或者从头开始构建这个项目:首先创建一个项目文件夹,运行npm init -y来设置项目基本结构,然后安装所需的依赖模块(@qvac/sdkexpresssocket.ioconcurrently),最后在package.json文件中将模块类型设置为"module"

1. 服务器配置(`server.js`文件)

创建一个名为`server.js`的文件,然后将以下代码粘贴到其中即可。import express from 'express';
import path from 'path';
import http from 'http';
import { Server } from 'socket.io';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { loadModel, unloadModel, getLoadedModelInfo, diffusion, SD_V2_1_1B_Q8_0 } from "@qvac/sdk";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
const server = http.createServer(app);
const io = new Server(server);

const PORT = process.env.PORT || 3000;

app.use(express.json());
app.use(express.static(path.join(__dirname, 'public');

const CONFIG_PATH = path.join(__dirname, '.device-preference.json');

function getPreferredDevice() {
try {
if (fs.existsSync(CONFIG_PATH)) {
const data = JSON.parse(fs.readFileSyncCONFIG_PATH, 'utf8'));
return data.device || null;
}
} catch (err) {
console.error('读取设备偏好设置失败:', err.message);
}
return null;
}

function setPreferredDevice(device) {
try {
fs.writeFileSync(CONFIG_PATH, JSON.stringify({ device }), 'utf8');
} catch (err) {
console.error('写入设备偏好设置失败:', err.message);
}
}

// 全局模型状态
let loadedModelId = process.modelId || null;
let modelLoadPercent = 0;
let modelLoadStatus = '等待触发...';
let isModelLoading = false;

const modelSize = (SD_V2_1_1B_Q8_0.expectedSize / (1024 * 1024 * 1024)).toFixed(2) + ' GB';

function broadcastModelProgresspercent, status) {
io.emit('model-download-progress', { percent, status, size: modelSize });
}

io.on('connection', (socket) => {
console.log('客户端已连接:', socket.id);

socket.on('disconnect', () => {
console.log('客户端已断开连接:', socket.id);
});

// 触发模型下载
socket.on('trigger-model-download', async () => {
// 如果模型已经加载完成,检查它是否仍在工作进程中
if (loadedModelId) {
try {
await getLoadedModelInfo({ modelId: loadedModelId });
socket.emit('model-download-progress', {
percent: 100,
status: '模型已成功在本地加载。',
size: modelSize
});
return;
} catch (err) {
console.log('模型ID无效或未找到,正在重置状态并重新加载..., ' + err.message);
loadedModelId = null;
process.modelId = null;
}
}

// 如果模型当前正在加载中,报告当前的加载进度
if (isModelLoading) {
socket.emit('model-download-progress', {
percent: Math.round(modelLoadPercent),
status: modelLoadStatus,
size: modelSize
});
return;
}

isModelLoading = true;
modelLoadPercent = 0;
modelLoadStatus = '开始加载模型...';
broadcastModelProgress(modelLoadPercent, modelLoadStatus);

try {
console.log('开始加载模型...'
const preferredDevice = getPreferredDevice();
const loadConfig = { prediction: "v" };
if (preferredDevice) {
loadConfig.device = preferredDevice;
if (preferredDevice === 'cpu') {
loadConfig.threads = 4;
}
console.log(`使用缓存的设备偏好设置:${preferredDevice}`);
}

loadedModelId = await loadModel({
modelSrc: SD_V2_1_1B_Q8_0,
modelType: "sdcpp-generation",
modelConfig: loadConfig,
onProgress: (p) => {
modelLoadPercent = p.percentage;
modelLoadStatus = p.percentage >= 100 ? '模型已成功在本地加载.' : `正在下载模型权重... (${p.percentage.toFixed(1)}%)`;
broadcastModelProgress(Math.round(modelLoadPercent), modelLoadStatus);
}
});
process.modelId = loadedModelId;

isModelLoading = false;
console.log('模型已成功加载。ID:', loadedModelId);
} catch (err) {
isModelLoading = false;
modelLoadPercent = 0;
modelLoadStatus = '加载模型失败: ' + err.message;
console.error('加载模型失败:', err);
broadcastModelProgress(0, modelLoadStatus);
socket.emit('error_event', { message: '加载模型失败: ' + err.message });
}
});

socket.on('generate', async (data) => {
const { prompt, ratio } = data;
if (!prompt || prompt.trim() === '') {
socket.emit('error_event', { message: '需要提供提示信息' });
return;
}

if (!loadedModelId) {
socket.emit('error_event', { message: '模型尚未加载完成' });
return;
}

const runDiffusion = async (modelIdToUse) => {
socket.emit('progress', {
percent: 0,
status: '开始执行扩散过程...'
sub: 'DIFFUSION INITIALIZING'
});

console.log(`正在使用模型ID ${modelIdToUse},为提示信息 „\({prompt}"“ 和比率 \){ratio} 生成图像`);

const { progressStream, outputs, stats } = diffusion({
modelId: modelIdToUse,
prompt,
});

// 输出每一步的进展情况
for await (const { step, totalSteps } of progressStream) {
const percent = Math.round((step / totalSteps) * 100);
socket.emit('progress', {
percent,
status: `当前步骤为 \({step}/\){totalSteps}…`,
sub: 'RUNNING DIFFUSION'
});
}

// 处理输出结果
const buffers = await outputs;
if (!buffers || buffers.length === 0) {
throw new Error('扩散模型未返回任何图像缓冲区');
}

// 将图像缓冲区转换为base64 Data URL格式,而不是将其保存到磁盘上
const base64Data = Buffer.from(buffers[0]).toString('base64');
const dataUrl = `data:image/png;base64,${base64Data}`;

// 发送成功信息
socket.emit('success', {
url: dataUrl,
prompt,
seed: (await stats).seed || -1
});

console.log(`图像已生成,并以base64 Data URL格式发送成功。`);
};

try {
await runDiffusion(loadedModelId);
} catch (err) {
console.error('图像生成失败:', err);

const isCrash = err.code === 50205 || (err.message && err.message.includes('WORKER_CRASHED'));
if (isCrash) {
console.log('在GPU上执行时工作进程崩溃了。正在尝试切换到CPU模式…');

// 保存设备偏好设置,以便下次直接使用CPU进行加载,避免重复加载
setPreferredDevice('cpu');

// 重置模型状态
loadedModelId = null;
process.modelId = null;

socket.emit('progress', {
percent: 0,
status: 'GPU驱动程序崩溃了。正在自动切换到CPU模式…',
sub: 'CPU FALLBACK LOADING'
});

try {
console.log('正在使用CPU加载模型…');
isModelLoading = true;
modelLoadPercent = 0;
modelLoadStatus = '正在加载CPU模型的权重…';
broadcastModelProgress(modelLoadPercent, modelLoadStatus);

loadedModelId = await loadModel({
modelSrc: SD_V2_1_1B_Q8_0,
modelType: "sdcpp-generation",
modelConfig: { prediction: "v", device: 'cpu', threads: 4 },
onProgress: (p) => {
modelLoadPercent = p.percentage;
modelLoadStatus = `正在加载CPU模型的权重… (${p.percentage.toFixed(1)}%)`;
broadcastModelProgress(Math.round(modelLoadPercent), modelLoadStatus);
}
});
process.modelId = loadedModelId;
isModelLoading = false;
console.log('模型已成功在CPU上加载。ID:', loadedModelId);

// 尝试再次使用CPU执行扩散过程
await runDiffusion(loadedModelId);
} catch (cpuErr) {
console.error('在CPU上执行切换失败:', cpuErr);
isModelLoading = false;
socket.emit('error_event', { message: '在CPU上生成图像失败:' + cpuErr.message });
}
} else {
if (err.message && (err.message.includes('MODEL_NOT_FOUND') || err.message.includes('未找到')) {
loadedModelId = null;
process.modelId = null;
broadcastModelProgress(0, '模型状态丢失。请重新触发加载过程.');
}
socket.emit('error_event', { message: '图像生成失败: ' + err.message });
}
}
});
});

app.get '*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

server.listen(PORT, () => {
console.log(`服务器正在运行,地址为 http://localhost:${PORT}`);
});

// 清理退出处理程序
async function handleCleanup() {
const modelId = process.modelId || loadedModelId;
if (modelId && modelId !== 'mock-model-id') {
try {
await unloadModel({ modelId, clearStorage: false });
} catch (err) {}
}
process.exit(0);
}

process.on('SIGINT', handleCleanup);
process.on('SIGTERM', handleCleanup);

2. 前端架构概述

由于我们的应用程序完全在本地运行,因此前端是一个使用纯HTML、CSS和客户端JavaScript构建的单页Web应用,它通过Socket.io WebSockets与Express服务器进行通信。

为了避免让本教程充斥着成百上千行的UI模板和样式表代码,我们将把重点完全放在后端的协调机制上。您可以从GitHub仓库中获取完整的HTML布局、Tailwind样式文件以及客户端脚本。

以下是客户端与服务器之间通信的具体流程概述:

  1. 预连接同步(trigger-model-download):页面加载完成后,客户端会建立WebSocket连接并发送trigger-model-download信号。服务器接收到该信号后,会检查模型是否已缓存或正在加载中,并开始发送进度信息。

  2. 去噪进程通知(progress):在图像生成过程中,服务器会持续发送包含去噪进展信息的事件(例如去噪步骤12/20...)。客户端会根据这些信息更新视觉进度条和状态提示。

  3. 数据URL传输(success):当所有去噪步骤完成后,服务器会将二进制图像缓冲区转换为Base64字符串,并发送success信号。客户端会直接将这个Base64数据URL应用于元素的源地址,从而实现图像的本地显示和立即下载。

代码结构解析

让我们来深入了解那些使我们的本地离线图像生成器能够顺利运行的关键机制。

1. 多客户端模型ID绑定过程(process.modelId

量化后的权重会占用大量内存。每次我们调用loadModel()方法时,QVAC都会启动一个独立的C++后台进程(即Bare工作线程)来运行GGML运行时环境。

为防止在客户端刷新页面或打开新标签页时多次生成相同的2.3GB GGUF模型文件,我们会在Node.js的process对象中全局存储已加载模型的ID:

let loadedModelId = process.modelId || null;
// ...
process.modelId = loadedModelId;

这种机制实际上实现了一个全进程范围的单一实例注册系统。但使用全局变量也会带来一个问题:失效的工作线程。如果某个客户端请求加载模型并获得了相应的ID,而后台工作线程随后崩溃或被终止,那么process.modelId仍然会保留这个无效的引用。

为了解决这个问题,每当有新的客户端连接并请求下载模型时,我们都会先通过getLoadedModelInfo方法来检查该模型的ID是否已经存在。

if (loadedModelId) {
  try {
    await getLoadedModelInfo({ modelId: loadedModelId });
    socket.emit('model-download-progress', { percent: 100, status: '模型已完全加载到本地。' });
    return;
  } catch (err) {
    console.log('模型ID无效,正在重置状态……', err.message);
    loadedModelId = null;
    process.modelId = null;
  }
}

如果后台工作进程已经终止,getLoadedModelInfo方法会抛出错误。异常处理块会捕获这个错误,清除无效的模型ID引用,并安全地重新启动加载流程。

[!重要提示] 进程单例机制的重要性:在开始推理计算之前,必须先检查模型状态的有效性。如果不进行验证,如果使用无效的模型ID调用diffusion()方法,会导致客户端连接超时,同时后台工作进程也会出现故障。

2. 内存中的图像序列化技术(零磁盘写入量)

将生成的图像文件保存到服务器的硬盘上会带来较大的I/O开销。此外,还需要编写自定义的定时清理脚本来删除旧的图像文件;而在用户访问量较大的系统中,这种做法还可能导致磁盘空间不足。

由于QVAC的diffusion()函数会将生成的PNG图像直接以内存二进制缓冲区的形式输出(即Uint8Array类型),因此我们完全绕过了本地文件系统。我们直接在内存中将这个二进制数组序列化为Base64字符串:

const base64Data = Buffer.from(buffers[0]).toString('base64');
const dataUrl = `data:image/png;base64,${base64Data}`;

这个Base64数据URL会通过WebSockets传输给客户端,客户端会立即将其用于显示图像:

  • 零磁盘写入量:服务器不会向硬盘写入任何数据,从而有效延长SSD的使用寿命,同时避免存储空间被浪费。

  • 即时交付:整个传输过程都在网络内存缓冲区中完成,因此不存在因磁盘序列化而导致的延迟。

  • 客户端集成十分便捷:

    客户端无需请求静态的图像URL路径,可以直接渲染Base64数据URL,从而让用户立即保存或下载图像。

3. GPU到CPU的备用方案及偏好缓存策略

在以本地设备为主的人工智能系统中,客户端硬件的多样性是一个巨大的挑战。例如,一些配备独立AMD Radeon显卡的旧款Intel Mac电脑虽然支持Apple的Metal框架,但却缺乏Stable Diffusion引擎所使用的现代张量运算功能,这会导致ggml-metal-ops.cpp文件中的C++代码出现崩溃。

为确保应用程序能够正常运行,并避免模型加载被重复执行(启动时在 incompatible GPU上加载一次,第一次请求失败后在CPU上再次加载一次),我们使用了一个持久的设备偏好缓存文件(文件名为.device-preference.json),并结合C++工作进程的崩溃检测机制来处理这些问题。

try {
  await runDiffusion(loadedModelId);
} catch (err) {
  const isCrash = err.code === 50205 || err.message.includes('WORKER_CRASHED');
  if (isCrash) {
    // 1. 将CPU优先设置缓存到磁盘上
    setPreferredDevice('cpu');

    // 2. 重置失效的引用值
    loadedModelId = null;
    process.modelId = null;

    // 3. 使用多线程技术自动在CPU上加载模型
    loadedModelId = await loadModel({
      modelSrc: SD_V2_1_1B_Q8_0,
      modelType: "sdcpp-generation",
      modelConfig: { prediction: "v", device: "cpu", threads: 4 }
    });
    process.modelId = loadedModelId;

    // 4. 自动重试模型生成过程
    await runDiffusion(loadedModelId);
  }
}

这种方案采用了双层防御机制:

  1. 动态恢复功能:如果GPU驱动程序出现故障导致系统崩溃,应用程序会捕获这一异常,将"device": "cpu"这个设置保存到.device-preference.json文件中,然后重新在CPU线程上加载模型参数,从而继续执行生成过程。用户只会看到状态更新信息,提示系统已切换到CPU模式运行,从而避免了原本可能发生的致命崩溃。

  2. 设置持久化功能:下次服务器启动或页面加载时,预加载机制会从磁盘中读取之前缓存的最佳配置选项,直接在CPU上加载模型。

const preferredDevice = getPreferredDevice(); // 从文件中读取设置
const loadConfig = { prediction: "v" };
if (preferredDevice) {
  loadConfig.device = preferredDevice;
  if (preferredDevice === 'cpu') {
    loadConfig.threads = 4;
  }
}
loadedModelId = await loadModel({
  modelSrc: SD_V2_1_1B_Q8_0,
  modelType: "sdcpp-generation",
  modelConfig: loadConfig,
  // ...
});

这种机制可以确保在后续会话中,服务器不会重复尝试在GPU上加载模型,从而避免浪费资源,同时也能保证模型只被加载一次,并且直接在合适的硬件平台上运行。

[!警告] CPU模式的延迟问题:虽然CPU模式能够在老旧硬件上保证系统的稳定性,但由于它使用的是顺序多线程计算方式,而不是GPU的并行计算能力,因此模型生成所需的时间会明显延长(在CPU上通常需要1到2分钟,而在兼容的GPU上只需10到15秒)。因此,在设计用户界面时,需要添加相应的进度显示功能,以便用户在系统切换到CPU模式时能够了解当前的运行状态。

结论

使用QVAC技术实现以本地计算为主的Stable Diffusion模型,可以让您完全掌控推理过程中的成本和数据隐私。通过将设备上的GGML模型与简单的Node.js WebSocket后端结合在一起,您可以开发出完全离线的Web工具,而无需花费任何费用来使用云服务。

随着移动设备和桌面系统中的芯片集成度越来越高,以本地计算为主的人工智能技术将会成为现代开发者越来越强大的工具。

资源与延伸阅读

Comments are closed.