一个基本问题:当我们谈论移植的时候,我们聊的是 ONNX 还是自定义算子?

这篇笔记不讨论哪条路线“更先进”,只记录一个实际选型问题:模型离开训练框架以后,究竟应该交给通用推理引擎,还是把它改写成项目自己的 C++ 实现。 面试时我常问这个问题。我关心的是,对方能不能把“能跑”和“适合长期维护”分开看。很多项目一开始只是照着现成方案接进去,等到包体积、启动时间或某个算子性能出了问题,才发现当初其实已经做了取舍。 所以这里的“移植”,不只是换一个文件格式。它更像是在两套成本之间做选择:一套成本由运行时和通用性承担,另一套成本由自己的代码、测试和维护承担。 一、ONNX 到底做了一件什么事 先从模型离开训练框架后的处境说起。 我们在 PyTorch 里训练模型,享受的是动态图、Python 生态、随时 print 随时调试。但部署环境——一个可能没有 Python、没有 torch、甚至没有文件系统的进程——什么都没有。训练与部署之间需要一次"交接"。交接最朴素的方式是序列化:把权重存下来,把结构描述出来。问题在于,每家框架的序列化格式都是私有方言:PyTorch 有 TorchScript 和 .pt,TensorFlow 有 SavedModel 和 .pb,互不相认。而部署端的推理引擎又层出不穷:ONNX Runtime、TensorRT、OpenVINO、CoreML、DirectML、各家 NPU 的私有 SDK……如果每个框架都要为每种引擎各写一条导出通道,对接复杂度是 N × M 的;在中间垫一层公共格式,它就变成 N + M。 ONNX(Open Neural Network Exchange)就是这层公共格式。具体来说,它负责三件事: 定义了一套标准算子集(operator set,简称 opset):Conv、Gemm、BatchNormalization、Relu……每个算子的语义、输入输出、数值行为都有书面规范,并按版本演进(opset 13、17、18……)。 定义了一张计算图格式:用 protobuf 描述节点(NodeProto)、张量(ValueInfoProto)、权重(Initializer),以及它们之间的连接关系。一个 .onnx 文件本质上就是一段序列化后的 protobuf。 定义了类型与张量规范:数据类型、布局约定、广播规则。 可以把它看成模型领域的交换文件:训练框架负责把图和权重写出来,推理引擎负责把它读进去。双方不必直接适配彼此,只要共同遵守这份格式即可。它能普及,主要也是因为这个生态位置,而不是因为它在每一种设备上都能做到最好。 这里有一个几乎所有初学者都会混淆、但一问就露馅的点:ONNX 只是一个格式,它自己不会执行任何东西。真正让模型跑起来的是推理引擎,最常见的就是 ONNX Runtime(下文简称 ORT)。前者是协议,后者是实现,就像 JSON 和某个 JSON 解析器的关系。我们日常说"用 onnx 跑",省略掉的主语几乎都是 ONNX Runtime。 那么这条路线的优势和代价分别是什么? 先看它为什么常常是默认选项。实际项目里,通常看重的是三点:格式标准,工具链成熟,以及性能能够满足要求。 标准:一次导出,处处可跑(至少在纸面上)。跨框架、跨引擎、跨操作系统。 成熟:ORT 背后是大量工程师十几年打磨的图优化器、算子融合 Pass,以及接驳成熟的 kernel 库(CPU 上有 MLAS 一系,移动端常用 XNNPACK)。你几乎不可能在同等工时内手写出比它更稳、覆盖面更广的实现。 快得足够:对绝大多数 PC 端、服务端场景,ORT 的默认性能已经在业务容忍范围之内;而且它提供 Execution Provider(EP)机制——CUDA、TensorRT、DirectML、CoreML、QNN——同一个 .onnx 几乎零改动就能下发到 GPU 甚至 NPU。 再说代价。代价不在"能不能跑",而在你为这份通用性交出的东西: ...

September 2, 2026 · 2 min · Konpaku Youran

PPG 情绪识别:几条路线的取舍与实测

PPG 情绪识别:几条路线的取舍与实测 写在前面 这个工作并不算solid,目前正在数据收集和实测阶段,涉及的数据集也只有WESAD和DEAP,并没有很强的参考意义,请谨慎参考。 摘要 要解决的问题很简单:在手表侧只有 PPG 和三轴加速度这两类输入的前提下,能不能先估计 valence 和 arousal,再映射到 Russell 情绪模型中的象限和离散标签,从而形成一条可闭环的情绪识别链路。 当前项目一共走过三条路线。第一条是 XGBoost Feature Baseline,它使用 HRV + ACC + SQI 这类手工特征,目标是先把任务跑通,并给后续复杂模型提供一个稳定参照。第二条是 EMCNN PPG-only,它直接建模单通道 PPG 波形,是当前的默认主方案。第三条是 EMCNN + aux,在 EMCNN 的基础上再加入 ACC/SQI 辅助输入,作为实验增强路线。 在当前 WESAD LOSO 实测下,EMCNN PPG-only 是综合表现最好的方案:它同时超过了 XGBoost 基线,并且比 EMCNN + aux 保持了更好的连续回归误差。EMCNN + aux 虽然在分类指标上更强,但 VA MAE 更差,更像分类增强方案。除此之外,本文还补充了推理性能、CPU/GPU、并发、内存和显存开销的实测,目的是把“能不能识别”和“值不值得部署”放在同一张桌子上讨论。 本文不公开项目实现和数据实测,仅作交流学习。 这个工作并不算solid,目前正在数据收集和实测阶段,涉及的数据集也只有WESAD和DEAP,并没有很强的参考意义,请谨慎参考。 一、这件事到底在做什么 当前项目的目标不是做一个泛泛的“情绪分类器”,而是要在手表侧有限传感器条件下,建立一条可以闭环的 Russell 情绪建模链路。输入是 PPG + ACC,中间层输出是连续的 valence 和 arousal,最后再映射到 Russell 情绪模型中的象限和离散标签。当前仓库中的推理接口已经对应到了这条链路: 连续层:valence / arousal 决策层:quadrant 展示层:21 个 Russell 标签 + Top-3 这里有一个必须讲清楚的前提:当前项目并不预设 PPG 能直接“表达情绪”,也不假设 PPG 和 valence/arousal 之间存在简单、稳定、显式的一一对应关系。更稳妥的说法是,情绪状态会通过自主神经系统影响心率、血管张力、外周循环和节律变化,而 PPG 恰好能观测到这些变化的一部分。因此,当前项目真正检验的是这样一个工程假设:PPG/ACC 中可能存在与 valence/arousal 相关的可学习统计模式。如果这个假设成立,就有机会在手表侧形成一套弱侵入、持续性的情绪状态估计能力。 ...

June 18, 2026 · 4 min · Leventure

ONNX - 它到底快在哪,又慢在哪

ONNX - 它到底快在哪,又慢在哪 一、ONNX 是什么 ONNX(Open Neural Network Exchange)本质上是一种模型的中间表示格式(IR),类似于编译器里的 LLVM IR。 用 PyTorch 训练完一个模型之后,模型的计算逻辑是用 Python 描述的,跑推理的时候要经过 Python 解释器、PyTorch 的调度器、再到底层的 CUDA kernel。这条链路很长,开销不小。ONNX 做的事情是:把模型的计算图从 PyTorch 的世界里"导出"成一个独立的、与框架无关的静态计算图,然后交给专门的推理引擎(比如 ONNX Runtime)去执行。 打个比方:PyTorch 训练出来的模型像一份 Python 脚本,每次执行都要解释器逐行翻译;ONNX 导出后的模型像一份编译好的二进制文件,直接跑就行。 一个典型的 ONNX 文件(.onnx)里面存的是: 计算图的拓扑结构(哪些算子、怎么连接) 每个算子的类型和参数(Conv、MatMul、Relu 等) 模型的权重(以 protobuf 格式序列化) 输入输出的 shape 和数据类型 这里要注意一点:ONNX 本身只是一个格式规范,它不负责执行。真正跑推理的是 ONNX Runtime(简称 ORT)或者其他兼容 ONNX 的推理引擎(TensorRT、OpenVINO 等)。说"用 ONNX 加速",实际上是"用 ONNX Runtime 加速"。 二、ONNX Runtime 为什么能快 理解了 ONNX 是什么之后,关键问题来了:为什么换个引擎跑就能快? 2.1 去掉了 Python 开销 PyTorch 推理时,即使模型本身的计算是在 GPU 上跑的,Python 层面仍然有大量开销: ...

March 13, 2026 · 3 min · Konpaku Youran

NSF-HiFiGAN-声码器学习笔记

从 HiFi-GAN 到 NSF-HiFi-GAN:声码器学习笔记 本文基于 RVC(Retrieval-based Voice Conversion)项目的实际代码,从零开始梳理 HiFi-GAN 声码器的原理,再过渡到 RVC 中真正使用的 NSF-HiFi-GAN 变体。 代码位置:infer/lib/infer_pack/models.py 和 infer/lib/infer_pack/modules.py 一、先搞清楚声码器在干什么 在语音合成或语音转换的流程里,声码器处在最后一环。它的上游会输出某种"中间表示"——可能是 mel 频谱图,也可能是某个隐空间的向量。声码器要做的事情就一件:把这个中间表示变回可以听的音频波形。 说得直白点:频谱图是一张"图",声码器要把这张图"念"出来。 传统做法(Griffin-Lim 之类的)靠数学迭代来恢复相位信息,结果通常比较糊。HiFi-GAN 走的是神经网络的路线——用一个生成器直接输出波形采样点,同时用判别器来监督生成质量。 二、HiFi-GAN 的基本结构 HiFi-GAN 的论文是 2020 年发的(Jungil Kong 等人),核心思路可以用一句话概括: 转置卷积做上采样,残差块做波形精炼,多尺度判别器做质量监督。 2.1 生成器的整体流程 在 RVC 的代码里,标准 HiFi-GAN 生成器对应的是 Generator 类(models.py:204)。它的结构其实很规整: HiFi-GAN 生成器结构(40kHz 配置为例) ┌─────────────────────────────────────────────────────────────┐ │ 输入:隐变量 z │ │ [batch, 192, T帧] │ └────────────────────┬────────────────────────────────────────┘ │ ▼ ┌────────────────────┐ │ 预处理卷积层 │ │ Conv1d(192→512) │ ← 把输入投射到高维空间 │ kernel_size=7 │ └────────┬───────────┘ │ ┌────────────┴────────────┐ │ 上采样 Stage 1 (×10) │ │ ConvTranspose1d(512→256)│ ← 时间轴拉长到 T×10 └────────┬─────────────────┘ │ ┌────────┴──────────────────────────────────┐ │ MRF (多感受野融合) │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐│ │ │ResBlock │ │ResBlock │ │ResBlock ││ │ │kernel=3 │ │kernel=7 │ │kernel=11 ││ │ └──────────┘ └──────────┘ └──────────┘│ │ 输出取平均 │ └────────┬──────────────────────────────────┘ │ ┌────────┴────────────┐ │ 上采样 Stage 2 (×10)│ │ ConvTranspose1d(256→128)│ ← 时间轴拉长到 T×100 └────────┬─────────────┘ │ ┌────────┴──────────────────────────────────┐ │ MRF 融合 │ │ ResBlock×3 (kernel=3/7/11) → 平均 │ └────────┬──────────────────────────────────┘ │ ┌────────┴────────────┐ │ 上采样 Stage 3 (×2) │ │ ConvTranspose1d(128→64)│ ← 时间轴拉长到 T×200 └────────┬─────────────┘ │ ┌────────┴──────────────────────────────────┐ │ MRF 融合 │ │ ResBlock×3 (kernel=3/7/11) → 平均 │ └────────┬──────────────────────────────────┘ │ ┌────────┴────────────┐ │ 上采样 Stage 4 (×2) │ │ ConvTranspose1d(64→32) │ ← 时间轴拉长到 T×400 └────────┬─────────────┘ │ ┌────────┴──────────────────────────────────┐ │ MRF 融合 │ │ ResBlock×3 (kernel=3/7/11) → 平均 │ └────────┬──────────────────────────────────┘ │ ┌────┴────────────┐ │ 后处理卷积层 │ │ Conv1d(32→1) │ ← 压缩到单声道 │ kernel_size=7 │ └────┬─────────────┘ │ ▼ ┌────────────┐ │ Tanh │ ← 限幅到 [-1, 1] └─────┬──────┘ │ ▼ ┌──────────────────┐ │ 输出波形 │ │ [batch, 1, T×400]│ └──────────────────┘ 上采样的总倍率 = 10 × 10 × 2 × 2 = 400。这个数字等于 hop_length(帧移),含义是:输入的每一帧对应输出的 400 个采样点。对于 40kHz 采样率来说,一帧就是 10ms。 ...

February 14, 2026 · 13 min · Konpaku Youran

rvc结构简介

RVC结构简介 推理流程 输入音频 (16kHz) │ ▼ ┌─────────────────┐ │ HuBERT │ 提取内容特征,输出256维(v1)或768维(v2) └────────┬────────┘ │ ▼ ▼ ┌─────────────────┐ │ F0 Extractor │ 提取基频,支持RMVPE/CREPE/Harvest/PM └────────┬────────┘ │ ▼ ▼ ┌─────────────────┐ │ Index Search │ 可选,用faiss做音色检索,混合特征 └────────┬────────┘ │ ▼ ▼ ┌─────────────────┐ │ Synthesizer │ VITS架构,生成目标音色波形 └────────┬────────┘ │ ▼ 输出音频 (32k/40k/48k) Synthesizer结构 主类是SynthesizerTrnMs256NSFsid(v1)和SynthesizerTrnMs768NSFsid(v2),区别只在输入维度。 SynthesizerTrnMs*NSFsid ├── enc_p (TextEncoder) │ ├── emb_phone: Linear(256/768 → hidden) │ ├── emb_pitch: Embedding(256, hidden) # pitch量化到256级 │ ├── encoder: Transformer Encoder │ └── proj: Conv1d → (mean, log_var) │ ├── flow (ResidualCouplingBlock) │ └── 4层 ResidualCouplingLayer + Flip │ 每层内部是WaveNet结构 │ ├── dec (GeneratorNSF) │ ├── m_source (SourceModuleHnNSF) │ │ └── SineGen: 根据F0生成正弦激励信号 │ ├── conv_pre │ ├── ups: 多级上采样 (ConvTranspose1d) │ ├── resblocks: HiFiGAN残差块 │ └── conv_post │ └── emb_g: Embedding(spk_num, gin_channels) # speaker embedding 推理时的数据流: ...

February 14, 2026 · 2 min · Konpaku Youran

GTCRN 轻量化的流式方案的演进思路

GTCRN 演进路径 记录 v1 → v2 → v3 → v3.1/v3.2 → v4 → v4.1 的改动和原因。 版本概览 版本 改动点 参数量 质量指标 内存 实时 v1 baseline 基线 139K DNSMOS 3.15 — × v2 transient 换损失函数 139K DNSMOS 3.15 — × v3 causal 因果化改造 145K DNSMOS 2.98 — √ v3.1 precision KD + QAT 压缩 41.6K PESQ 2.041 228 KB (INT8) √ v3.2 transient 宽度1.5× + 瞬态损失 ~83K PESQ ~2.15 ~355 KB (INT8) √ v4 network opt 架构精简 (4层GTConv) ~87K PESQ 2.147 683 KB (FP32) √ v4.1 int8 INT8 混合精度 C 推理 ~87K PESQ 2.037 464 KB √ 网络结构 (v1/v2 共用) 输入 spec (B, 513, T, 2) │ ├─ 可学习频带权重 (513,) │ ▼ ERB_48k.bm(): 513 → 219 │ 低频171保留,高频342→48 ERB band │ ▼ SFE_Lite: DWConv(1×5) → PWConv → BN │ ▼ ┌─ Encoder ─────────────────────────────┐ │ DSConv: 219→110 (stride=2) ← skip1 │ │ DSConv: 110→55 (stride=2) ← skip2 │ │ GTConvLite×6 (d=1,2,4,8,4,2) ← skip3-8 │ SubbandAttention │ └───────────────────────────────────────┘ │ ▼ DPGRNN_Enhanced × 2 │ intra: 双向GRU (频率轴) │ inter: 单向GRU (时间轴) │ ▼ ┌─ Decoder ─────────────────────────────┐ │ GTConvLite×6 + skip (逆序) │ │ DSDeconv: 55→110 + skip2 │ │ DSDeconv: 110→219 + skip1 │ └───────────────────────────────────────┘ │ ▼ ERB_48k.bs(): 219 → 513 │ ▼ CRM掩码 → 输出 GTConvLite 内部 x → DWConv(3×3, dilation) → PWConv → BN → PReLU → TRALite (时序注意力) → SEBlock (通道注意力) → + x (残差) DPGRNN 内部 x (B,C,T,F) → reshape (B*T, F, C) → Linear → 双向GRU (频率轴) → Linear → reshape + LayerNorm → reshape (B*F, T, C) → Linear → 单向GRU (时间轴) → Linear → reshape + LayerNorm → 输出 v1 → v2: 换损失函数 问题 v1 用的是标准 SpecRIMAGLoss,对所有帧一视同仁。但实际听感上,键盘敲击、鼠标点击这类突发噪音处理得不好。DNSMOS 是整段平均,掩盖了这个问题。 ...

February 13, 2026 · 7 min · Konpaku Youran

GTCRN轻量化方案

GTCRN-Light v3 技术说明书 0. 扼要(Executive Summary) GTCRN-Light v3(以下简称 v3)是在原生 GTRCN 基础上进行的等价轻量化实现:完整保留“ERB→SFE→Encoder(频轴两次 /2)→DPGRNN(intra→inter)→Decoder(镜像+跳连)→ERB⁻¹→复域 CRM”的主干数据流与功能语义,通过算子级设计收缩参数与 MACs,同时增强形状稳定性与工程可部署性。 核心收益: 结构等价:无语义重构、无路径删减;对齐原版的时/频建模顺序与接口。 计算瘦身:卷积 DW-Separable 化、RNN 低秩瓶颈、门控去 RNN 化、ERB 固定权重化。 工程稳态:严格的频轴上/下采样闭环(33→65→129),对齐安全,易于导出与部署。 1. 设计目标与边界(Design Goals & Constraints) 不改变 GTRCN 的任务假设与编解码语义:复域 CRM、ERB 子带、频轴二次下采样、DPGRNN(先 intra 后 inter)、镜像解码与跳连。 降低参数与 MACs,但不牺牲 DPGRNN 的双路径长程/跨频建模。 形状稳定:频轴整数对齐,杜绝奇偶差累积;跳连前天然同维。 部署友好:避免难以量化/导出的算子(极小化状态化 RNN、减少不必要的线性层)。 2. 与原生 GTRCN 保持一致的“架构不变量” 数据流: (B,F,T,2) → [|S|, Re, Im] → ERB(bm) → SFE → Encoder(freq /2 ×2) → DPGRNN(intra→inter) → Decoder → ERB(bs) → CRM × S(复域) 采样策略:ERB 后 F=129;编码两次在频轴 /2:129→65→33;解码反向:33→65→129(确保 33→65→129 的闭环)。 时/频耦合:瓶颈处严格遵循 intra-(per time, across F) → inter-(per freq, across T) 的双路径顺序。 输出语义:预测 CRM(实/虚) 并在复域与输入逐点相乘。 3. 轻量化的四大支柱(Pillars of Lightweighting) 3.1 卷积主干 DW-Separable 化 + 轻量 GT-ConvLite 动机:将 2D 卷积的通道耦合与空间(T/F)卷积解耦,保留感受野与局部子带建模能力的同时,将参数与 MACs 近似按 1/通道数 降低。 ...

February 13, 2026 · 3 min · Konpaku Youran