Mizuoto

运行时

引擎怎样在 Mizuoto 的进程里运行:运行时的各部分(设备、模型句柄与管理器、层、注意力、采样),请求上下文,引擎不得改动全局状态的规则,LocalEngine,内存的各个层次,取消与超时,流式,远程引擎,以及查看这一切的命令。

每个引擎都在服务器自己的进程里运行(在命令行上则在命令自己的进程里),基于 Mizuoto 自己的运行时:mizuoto/runtime/,一个仿照 ComfyUI 模型管理方式写的小框架。核心只通过 mizuoto/core/inference/ 接触它。本页讲两边:引擎从运行时得到什么,以及核心怎样调用一个已加载的模型。怎样编写引擎见接入引擎。

谁在哪里

部分代码作用
InferenceServicemizuoto/core/inference/service.py第一次用到时按 compute 设置建立 Runtime;回答本机实现了哪些引擎、某个引擎缺什么(torch、mizuoto-fishspeech 等)、它的附加依赖和训练器
RuntimeServicemizuoto/core/inference/runtime.py每个(引擎,设备)一个引擎,按需创建;loaded() 给出模型的 EngineHandle;max_resident_models、空闲回收、runtime_changed、doctor
LocalEngine / LocalHandlemizuoto/core/inference/local.py本进程中一个设备上的一个引擎,以及它某个已加载模型的句柄
RemoteEngine / RemoteEngineHandlemizuoto/core/remote/client.py同上,用于名为 <主机>/<设备> 的设备:与另一台机器上 mizuoto agent 的会话
EngineHandle、EngineRequest、ModelRefmizuoto/core/inference/engine.py核心持有和传递的东西:模块顶层只用标准库
任务类型mizuoto/core/inference/tasks.pySynthesisTask、VoiceSpec、Reference、LoadSpec、AudioChunk、Description 等(标准库数据类)
Engine、LoadedEngine、注册表mizuoto/runtime/engine.py引擎契约;BUILTIN 和 mizuoto.engines 入口点
RequestContextmizuoto/runtime/context.py每次调用一个:取消、生成器、进度、文本片段、临时目录
Runtimemizuoto/runtime/runtime.pyEngine.load 收到的东西:devices(DeviceService)、manager(ModelManager)、options(RuntimeOptions)

TtsService、AsrService 和音色服务从不直接接触 LocalEngine:它们调用 RuntimeService.loaded(ModelRef, device, dtype, options, request),使用它返回的句柄。本地还是远程对它们没有区别。

运行时

InferenceService.runtime 在第一次加载模型时为每个进程建立一个 Runtime,所以从不加载模型的命令不会导入其中任何部分。

设备:device.py

DeviceService 相当于 ComfyUI model_management.py 中设备相关的那一半,只是做成了对象而不是模块全局变量。设备名用 Mizuoto 的写法:cpu、cuda:N(NVIDIA,或 ROCm 上的 AMD)、xpu:N、mps;解析 auto 是核心的事(ComputeService.resolve_device)。引擎拿到的是真正的 torch.device;没有任何东西去收窄 CUDA_VISIBLE_DEVICES。

  • memory(device):(空闲,总量)字节数;在 CUDA 上,torch 占着但没用的显存也算空闲。
  • empty_cache(device)、synchronize(device)。
  • supports(device, dtype) 和 pick_dtype(device, requested, supported, cpu):组件用哪种精度运行,依据用户的 engines.<id>.dtype 和引擎给出的偏好顺序(CPU 上除非指定都用 fp32;GTX 16xx 显卡不用 fp16)。
  • is_oom(error):一个异常是否表示设备内存不足,不导入 torch。
  • float64_device(device):以 float64 计算的模块在哪里运行——该设备,或在没有 float64 的设备(MPS、不支持 fp64 的 XPU)上改用 CPU。pin_to_cpu(module) 让这样的模块在父模块移动时留在 CPU 上;CosyVoice 3 的 F0 预测器用到这两者。
  • probe_devices():torch 看到的加速器(CUDA、通过 HIP 的 ROCm、XPU、MPS)及其显存。核心的设备探测(mizuoto/core/compute/probe.py)先询问 nvidia-smi、rocm-smi/amd-smi 和平台,只有在已安装的构建可能找到它们没找到的 GPU 时,才在子进程中询问 torch(python -m mizuoto.runtime.device);运行时加载了 torch 之后则在本进程中询问。
  • 引擎的 DEVICES(默认为所有设备类型)与其清单的 devices 一致;在 MPS 上 torch.compile 不生效,按原样运行。
  • torch_build():已安装的 torch 是为什么构建的、本机哪些加速器可用(mizuoto doctor 用它)。

模型句柄与模型管理器

ModelHandle(handle.py,相当于去掉扩散部分的 ComfyUI ModelPatcher)包装一个带权重的 nn.Module:name(显示在已加载模型列表中,如 omnivoice/omnivoice:model)、load_device、offload_device(CPU)、kind(自回归语言模型为 sequence,其他为 oneshot)以及各项大小(size_bytes、loaded_bytes、streamed_bytes、host_bytes)。引擎只读取 module;LoadedEngine 在 self.handles 中列出它的句柄。

ModelManager(manager.py,相当于 ComfyUI 的 load_models_gpu / free_memory)记录每个已注册句柄的所有者(<model id>@<device>)和最后使用时间,并在请求的每个阶段(ctx.use、ctx.run)把句柄整体或部分放到设备上:空闲组件逐个单元让出显存,放不下的模型把部分权重留在内存中、在各层运行时流式加载到设备,显存不足的阶段以更多权重流式加载的方式精确地重新运行。策略(policy.py,VramPolicy)在每次加载时由 compute 设置得出。

方法用途
register(owner, handles, mode, pinned) / unregister(owner)加载之后;卸载模型时(直接释放,不复制回来)
use(handles, memory_required)包在每个阶段外;没有 PHASED 的引擎则包住整个调用
after_oom(handles)ctx.run 重试前的清理
release_idle(device)、drop_host_copies()腾空一块 GPU(微调前、显存不足后);内存不足时
resident()、owner_usage(owner)、owner_placements(owner)已加载模型列表、meta.memory 及其 partial 报告

阶段、预算与显存模式、单元与流式加载、主机副本、让出显存、精确重试、工作显存估算及其校准,以及引擎要怎样配合:见显存与内存。

在 CPU 上什么都不移动、不计数;除非 DeviceService(simulate={"cpu": bytes}) 让它扮演一块 GPU(测试与一致性验证工具:compute.simulate_vram_mb)。

层、注意力、加载

  • ops.py:每个引擎都用 ops 命名空间而不是 torch.nn 来搭建模块。ops.default 里是同样的层,但不做随机初始化:构造一个大模型不花时间,也不抽随机数,所以加载永远不会碰到任何随机数生成器。ops.cast 还会在每次前向时把权重转换成输入的精度和设备,用于以一种精度存储、以另一种精度计算的组件。init_random(module, generator) 在测试里给 default 模块填上真正的权重。
  • accel/:原生代码中的每一次注意力计算都写作 accel.attention(q, k, v, *, site=SITE, mask=…, causal=…, scale=…, enable_gqa=…, dropout_p=…, mask_info=…),从不直接调用 SDPA(架构测试禁止在 runtime/accel 之外这样做)。每个 Site 保存它的参照计算,即上游在该处的精确计算;compute.attention = auto(默认)时运行参照计算,结果逐位相同。其他取值会绑定一个策略,按调用在 SDPA 的各内核、FlashAttention、xFormers 和 SageAttention 之间选择,带兼容性检查和回退;features.apply 设置主机的 compute.fast 开关,也是原生代码中唯一设置 torch.backends 标志的地方。见加速。
  • loading.py:load_state_dict(safetensors,或以 weights_only=True 读取的 pickle)、strip_prefix、replace_prefixes、fold_weight_norm,以及默认严格的 load_into:缺少或多出的键都是错误,并写明是哪个键,除非引擎把它列为可忽略。
  • sampling.py:hf_sample / hf_process,按 transformers 的顺序执行它的 logits 处理器(重复惩罚、最少新 token、屏蔽、温度、top-k、top-p),然后是 multinomial1:在请求的生成器上执行 torch.multinomial 自己的算法,但不等待设备,所以带种子的抽样结果一致(无同步采样)。
  • lm/:QwenDecoder(带 transformers 数值行为和参数名的 Qwen2/Qwen3 解码器,以及 mask_for、hf_sdpa)、DynamicCache、需主动开启的 BufferCache(compute.kv_cache = static),以及用于工作显存估算的 peak_bytes。
  • graphs.py:Graphs、LayerSteps、DecoderSteps:把解码步骤捕获为 CUDA Graph,重放即时执行的内核(compute.cuda_graphs),见 CUDA Graph。
  • convert/onnx.py:直接从 protobuf 线格式读出 ONNX 图的节点和张量,不需要 onnx 包(CosyVoice 的语音分词器和 CAM++、GPT-SoVITS 的 G2PW 用到)。
  • compile.py:compute.compile 打开时,Compiled / compile_module(module, enabled, name) 把一个热点模块包进 torch.compile(dynamic=True);引擎调用 compiled or module,编译器出错时改为即时执行。见 torch.compile。
  • policy.py、units.py、stream.py:显存策略、模型可流式加载的单元,以及每个设备一个的 Streamer(显存与内存)。
  • snapshot.py:snapshot() 对引擎调用必须保持不变的进程级状态做摘要;契约套件在请求前后比较它。

引擎共用的构件放在运行时旁边:mizuoto/nn(与 torchaudio、librosa、Whisper 逐位一致的 DSP,流模型,transformer,声码器,编解码器,SSL 和说话人编码器)和 mizuoto/text(对 Mizuoto 运行的模型与 transformers 结果一致的 BPE 分词器、Whisper 词表、wetext 规范化器)。

请求上下文

对引擎的每次调用都有一个 RequestContext(mizuoto/runtime/context.py):

成员用途
check_cancel()、cancelled()协作式取消。check_cancel() 抛出 Cancelled,它是 BaseException,模型代码里的 except Exception 吞不掉它。引擎在每个 token 或每个 ODE 步调用它
bind()把上下文设为当前线程的请求;层里于是可以直接调用模块级的 check_cancel(),不必传参
generator_for(device)、generator请求在某个设备上的 torch.Generator,每个设备一个,用请求的种子初始化(没有种子则用新的熵)
rng请求的 random.Random,同样方式初始化
reseed(seed)让请求的随机流从头开始(像 GPT-SoVITS 上游那样每段重新播种的引擎会用)
progress(stage, value, message)loading:<部分>、generating 等
applied、warn(message)引擎实际使用的参数值和它的警告:两者都随结果返回
text_pieces()客户端发来的流式文本(SynthesisTask.text_stream):一个真正的生成器,可以在别的线程上消费,取消时提前结束
tmpdir<cache>/scratch/<引擎>/ 下的临时目录,调用结束时删除
use(handles, memory)、run(name, handles, fn, memory, generators, fallback)请求的一个阶段:放上它的组件并为其工作显存留出空间;run 另加显存不足后的精确重试和可选的回退(显存与内存)
accel模型的注意力策略,随上下文绑定(加速)
log、device、seed、request_id

全局状态

引擎与服务器、彼此以及测试共用一个进程,所以原生代码不改动任何进程级状态。test_native_code_leaves_global_state_alone 在 runtime、nn、text、各引擎和需要接受许可的包里禁止:

  • 给全局随机数生成器播种:torch.manual_seed、torch.cuda.manual_seed(_all)、random.seed、np.random.seed;
  • os.chdir、sys.path.append / insert、os.putenv、给 os.environ 赋值;
  • logging.basicConfig、torch.set_default_dtype、torch.use_deterministic_algorithms、给 torch.backends.* 和 torch._inductor.config 赋值。

随机数改从 ctx.generator_for(device) 和 ctx.rng 取。这也是对照得以成立的前提:一个用种子 s 初始化的全新 torch.Generator,与 torch.manual_seed(s) 之后 torch 的默认生成器产出完全相同的序列,在 CPU 和 CUDA 上都是如此(scripts/parity/check_rng.py)。所以一个引擎只要在请求的生成器上按上游的顺序做上游的抽样,就能逐次复现上游带种子的运行。上游在构建或准备某个东西时从全局生成器抽过随机数的地方,移植版会在私有生成器上重放这些抽样,而不去碰全局生成器。

训练阶段是唯一的例外:它们在自己的进程里运行,并在那里给进程级生成器播种(见训练器)。

LocalEngine:在本进程中调用模型

RuntimeService.engine(engine_id, device) 在第一次用到时创建一个 LocalEngine,并把它作为显存持有者 engine:<引擎>:<设备> 登记到 ComputeService。它的状态是 stopped(什么都没加载)、ready、busy,或者引擎实现导入失败时的 crashed。

加载(LocalEngine.load):

  1. start() 用 get_engine(id) 找到实现(导入它的模块,不导入 torch)。
  2. make_room(model_id):运行时服务卸载超出 compute.max_resident_models 的、最久未用的空闲模型(固定的模型保留)。
  3. 构造 LoadSpec(model_id, model_dir, variant, device, dtype, options);options 合并了引擎的加载选项和 model_options(model_id),后者把模型所需共享资源的目录放进 options["assets"]。
  4. 在绑定了上下文、不在 torch.inference_mode() 里的情况下运行 Engine.load(spec, runtime, ctx);句柄登记到管理器,describe() 的结果保存为句柄的 description。

调用(LocalHandle.prepare_voice、synthesize、convert、edit、call):每次都在调用线程(某个任务的线程)上、在 torch.inference_mode() 里运行,上下文已绑定,模型的句柄由 manager.use(...) 占住。种子取自任务的 params["seed"]。

为什么加载不在推理模式里

这是对照 E2-TTS 与上游时发现的一个 CPU 内核陷阱:F.linear 作用于跨步(非连续)的三维输入时,若权重需要梯度,或权重是推理张量(在 torch.inference_mode() 里创建的),就会走另一个内核,即使在 no_grad 下也是如此。上游的权重是普通的、需要梯度的参数,所以原生引擎保持 nn.Module 创建参数时的样子(不调用 requires_grad_(False)),模型也在推理模式之外加载。请求本身仍在推理模式里运行。两个陷阱的细节见引擎参考。

流就是生成器

synthesize、convert 和 edit 返回产出 AudioChunk 的生成器。LocalEngine.stream 惰性地驱动引擎自己的生成器:只有调用方索取下一块时引擎才运行,也只有这时它才绑定到线程、处在推理模式里。每一块都会被检查(必须是 AudioChunk),转成 float32 单声道 numpy 采样再交出去;第一块设定 timings["first_chunk"]。调用方关闭流(contextlib.closing)就能提前停止,引擎的生成器也随之关闭。TtsService 逐块取出,客户端流式接收时再把每块转给 StreamSink。

取消与超时

调用方传入一个 EngineRequest,带着它的 cancel 事件(任务的 cancel_event)、可选的 timeout、流式文本队列(text_input,由 TextFeed 填充)和 progress 回调。一个小的监视线程每 20 毫秒检查一次:把调用方的取消转到上下文的取消事件上;超时则把这次调用标为超时并取消它。引擎在下一次 check_cancel() 时停下;调用随之以 CancelledError 失败,超时则是 OperationTimeoutError。调用结束时,句柄把 applied、warnings 和 timings["total"] 写回 EngineRequest。

错误

引擎抛出的东西会变成核心的错误:

  • EngineError(message, detail, code) 保留它的错误码(error_from_code),并在 detail 里加上引擎和设备;Unsupported 是 capability_not_supported。
  • 内存不足(is_oom)时,把该设备上所有空闲组件移走(release_idle),并以 compute_unavailable 失败,detail.reason = "out_of_memory";任务服务随后卸载所有 GPU 上未在使用的模型(release_after_oom)。
  • 其他任何异常都是 engine_error,detail 里(以及日志里)有异常名和回溯。

内存

内存在三个层次上管理:

层次谁做什么
组件ModelManager调用的每个阶段之前,同一设备上的空闲组件逐个单元让出显存腾地方;放不下的部分从内存流式加载(显存与内存)
模型RuntimeService所有引擎合计最多保留 compute.max_resident_models(默认 3)个已加载模型;加载前卸载最久未用的空闲模型。服务器的回收线程(每 10 秒)还会卸载空闲超过 compute.unload_after_minutes(默认 20;0 表示从不)或 engines.<id>.idle_minutes 的模型。engines.<id>.pinned_models 既不会为腾地方而卸载,也不会因空闲而卸载
进程ComputeService每个 LocalEngine 都是一个显存持有者:释放显存(POST /api/v1/compute/release)卸载所有空闲持有者的全部模型并移走剩下的东西;有持有者忙时拒绝

不会因为对显存的估算而拒绝加载模型:模型总是尝试加载,只有显存分配真正失败才算错误。任务因显存不足失败时(精确重试之后),ComputeService.release_after_oom 卸载所有 GPU 上未在使用的模型(即 ComfyUI 的 unload_all_models);CPU 上的模型保留。在这一层之下组件怎样共用一个设备,见显存与内存。

任何 engines.<id>.* 设置的改动都会卸载该引擎的模型(unload_engine),下一个请求用新的设备、精度或加载选项重新加载。

远程引擎

当一个引擎的设备是 <主机>/<设备>(例如 engines.qwen3tts.device = "gpu-box/cuda:0")时,RuntimeService.engine 向 RemoteService 要一个 RemoteEngine:与那台主机上的 mizuoto agent 之间、针对一个设备上一个引擎的会话。agent 是一个完整的 Mizuoto,有自己的数据目录、模型和运行时;它在自己的进程里运行引擎,和主机完全一样。RemoteEngineHandle 与 LocalHandle 回应同样的调用。面向用户的配置见远程引擎。

协议 2(mizuoto/core/remote/wire.py 的 AGENT_PROTOCOL)在“已加载引擎”这一层工作。帧格式来自 mizuoto/core/remote/frames.py(4 字节长度、一个类型字节、JSON 正文,或 JSON 头加原始字节),走 TCP 或 TLS:

  1. 主机连上后发送 hello,带令牌(用 hmac.compare_digest 比较)和协议版本。协议不同的 agent 会拒绝主机,提示在两台机器上安装相同版本的 Mizuoto;主机也同样拒绝这样的 agent。否则 agent 回复它的 AgentInfo:版本、设备、那里哪些引擎可用,以及已安装的模型。
  2. open {engine, device},然后依次执行 load、prepare_voice、synthesize、convert、edit、call、unload 和 status;cancel 和 text(流式文本)随时可以发。
  3. agent 对每个操作先回 progress、chunk(二进制帧:float32 小端单声道)和 file 帧,最后是 result 或 error,并每 5 秒发一次心跳。超过 remote.heartbeat_timeout(60 秒)收不到任何帧,主机就以 worker_failed(during = "heartbeat")关闭会话;下一个请求重新连接。

按内容传文件。 路径在另一台机器上没有意义。请求里提到的每个文件都变成 mizuoto-file:<sha256>/<文件名>:have 询问 agent 缺哪些,put 以 8 MiB 的帧发送缺的那些,并核对 SHA-256。agent 把它们存在 <cache>/remote/files/,一个月没有主机要过的文件会被删除。prepare_voice 写入的目录变成 mizuoto-out:…,其中的文件作为 file 帧传回主机自己的目录。

主机依据自己的模型目录和 agent 的模型列表做规划;agent 自己处理它的显存,训练始终在本机进行。

查看:mizuoto runtime 与 mizuoto doctor

mizuoto runtime                 # 正在运行的服务器加载了哪些模型:引擎、设备、字节数、忙或空闲
mizuoto runtime --unload        # 卸载所有没在用的模型
mizuoto runtime qwen3tts/1.7b-base --unload
mizuoto doctor                  # torch 与硬件是否匹配、每个引擎、附加依赖
mizuoto doctor gptsovits --json
  • mizuoto runtime 询问正在运行的服务器(通过数据目录里的 server.json 找到它),因为模型在服务器进程里;没有服务器时报告命令自身进程的情况。对应 GET /api/v1/runtime 和 POST /api/v1/runtime/unload,以及 Web UI 的内存中的模型面板(LoadedModelsSheet)。
  • mizuoto doctor [ENGINE] 就是 RuntimeService.doctor:已安装的 torch 构建与本机硬件(CUDA、ROCm、XPU、MPS、CPU)是否匹配,并给出安装正确构建的命令(mizuoto/core/compute/torch_index.py);每个引擎可用还是缺失(以及缺什么);导入不了的可选附加依赖(Engine.EXTRAS);以及训练器缺的依赖。对应 GET /api/v1/runtime/doctor 和 GET /api/v1/engines/{id}/doctor。

本页目录