elec-ops-inspection 解决的是「看图找故障」,elec-ops-prediction 解决的是「看数据预判未来」——用电负荷预测、变压器故障预警、风电/光伏发电量预测。跟 CV 不同,电力预测的核心是时序模型:LSTM、Transformer、TCN(时序卷积网络),输入是历史序列(过去 72 小时的负荷/温度/湿度),输出是未来 24 小时的预测值。

电力预测的三类任务

任务 模型 输入序列 预测长度 精度要求
短期负荷预测 TCN + 注意力 168 点 (7天×24h) 24 点 MAPE < 3%
变压器故障预警 Transformer 时序 720 点 (30天×24h) 分类 (正常/预警) F1 > 0.9
光伏发电量预测 LSTM + 天气编码 72 点 (3天×24h) + 气象数据 24 点 MAPE < 8%

三类任务共用一个训练框架,但部署时的模型结构不同。

时序模型在 NPU 上的挑战

CV 模型的瓶颈是矩阵乘(Cube 单元),时序模型的瓶颈在序列处理——LSTM 的逐元素 gate 计算(Vector 单元)、Transformer 的 self-attention 在长序列上的 O(N²) 显存需求。

// elec-ops-prediction/ops/tcn_kernel.cpp
// TCN(时序卷积网络)的核心:膨胀卷积 + 因果卷积

__aicore__ void TCNBlock(
    GlobalTensor<float>& output,       // [B, T, C] 输出
    GlobalTensor<float>& input,        // [B, T, C] 输入
    GlobalTensor<float>& weight,       // [kernel_size, C, C] 权重
    int batch, int time_steps, int channels,
    int dilation                       // 膨胀因子:随层数指数增长
) {
    // 膨胀卷积:卷积核的元素不是连续的,
    // 而是每隔 dilation 个时间步取一个
    // dilation=1: [t-2, t-1, t] 三个时间步
    // dilation=2: [t-4, t-2, t] 三个时间步
    // dilation=4: [t-8, t-4, t] 三个时间步

    const int kernel_size = 3;

    for (int t = 0; t < time_steps; t++) {
        LocalTensor<float> out_channel(channels);
        out_channel = 0.0f;

        for (int k = 0; k < kernel_size; k++) {
            // 膨胀访问:t - k * dilation
            int src_t = t - k * dilation;
            // 因果卷积:只看过去,不看未来
            if (src_t < 0) continue;

            // 加载 src_t 时间步的全部通道
            LocalTensor<float> in_slice;
            GatherTimeStep(input, in_slice, src_t, channels);

            // 第 k 个滤波器的权重矩阵 [C, C]
            LocalTensor<float> w_slice;
            GatherKernel(weight, w_slice, k, channels);

            // 逐通道线性变换
            // 矩阵乘 [1, C] × [C, C] → 用 Cube 单元
            out_channel = MatMul_FMA(out_channel, in_slice, w_slice);
        }

        DataCopy(output, out_channel, t * channels, channels);
    }
}

TCN 的优化关键是膨胀卷积的时间步访问模式——dilation 越大,时间步之间的间距越大,L1 缓存的复用率越低。dilation=16 时每加载一个时间步,前一个时间步的缓存大概率已失效。

LSTM 的 gate 计算在 Vector 单元上的展开

LSTM 的四个 gate(input/forget/cell/output)是逐元素操作——天生适合 Vector 单元的 256 lanes 并行:

// elec-ops-prediction/ops/lstm_kernel.cpp

__aicore__ void LSTMStep(
    GlobalTensor<float>& h_t_out,      // [B, hidden] 新隐状态
    GlobalTensor<float>& c_t_out,      // [B, hidden] 新细胞状态
    GlobalTensor<float>& x_t,          // [B, input_dim] 当前输入
    GlobalTensor<float>& h_t1,         // [B, hidden] 上一时刻隐状态
    GlobalTensor<float>& c_t1,         // [B, hidden] 上一时刻细胞状态
    GlobalTensor<float>& W_ih,         // [4*hidden, input_dim] 输入权重
    GlobalTensor<float>& W_hh,         // [4*hidden, hidden] 隐状态权重
    GlobalTensor<float>& b_i,          // [4*hidden] bias
    int batch, int hidden
) {
    // LSTM 的四个 gate 共享 W_ih 和 W_hh——先统一算 gate 的线性部分
    // gates = x_t × W_ih^T + h_t1 × W_hh^T + b_i

    LocalTensor<float> gates(4 * hidden);  // 4 个 gate 拼在一起

    // 第一步:线性变换(用 Cube 单元做矩阵乘)
    // x_t [B, input_dim] × W_ih^T [input_dim, 4*hidden] = [B, 4*hidden]
    LocalTensor<float> x_proj;
    MatMul(x_proj, x_t, Transpose(W_ih));

    // h_t1 [B, hidden] × W_hh^T [hidden, 4*hidden] = [B, 4*hidden]
    LocalTensor<float> h_proj;
    MatMul(h_proj, h_t1, Transpose(W_hh));

    // 加 bias
    gates = x_proj + h_proj + b_i;

    // 第二步:拆分成 4 个 gate + 激活函数
    // Vector 单元并行处理每个 gate
    LocalTensor<float> i = Sigmoid(Slice(gates, 0 * hidden, hidden));
    LocalTensor<float> f = Sigmoid(Slice(gates, 1 * hidden, hidden));
    LocalTensor<float> g = Tanh(Slice(gates, 2 * hidden, hidden));
    LocalTensor<float> o = Sigmoid(Slice(gates, 3 * hidden, hidden));

    // 第三步:细胞状态更新(逐元素,Vector 单元)
    // c_t = f * c_t1 + i * g
    c_t_out = f * c_t1 + i * g;

    // 第四步:隐状态更新
    // h_t = o * tanh(c_t)
    h_t_out = o * Tanh(c_t_out);
}

四个 gate 的数据复用:W_ih 和 W_hh 只加载一次,四个 gate 的线性变换结果 gates 在 L1 上直接被四次激活函数消费。全部在片上完成——没有中间变量写回 HBM。

踩坑一:TCN 膨胀因子越大,节省的 HBM 带宽越少

dilation 从 1 推到 16 的过程中,时间步之间的 L1 复用率急剧下降:

dilation 相邻时间步间距 L1 可复用数据 有效计算时间占比
1 1 time step 前一步的通道数据在 L1 82%
2 2 time steps 每两步才复用一次 64%
4 4 time steps L1 缓存大概率过期 45%
8 8 time steps 几乎无复用 28%
16 16 time steps 无复用,每次从 HBM 加载 18%

dilation=16 时有效计算时间占比掉到 18%——剩下 82% 的时间在搬数据。

优化策略:在 dilation≥4 的层上,预先把所有时间步的数据一次性加载到 L1(假设 L1 能装下),然后做任意 dilation 的膨胀卷积——不再需要从 HBM 逐步加载。

// 优化:预加载全部时间步到 L1
if (dilation >= 4 && time_steps * channels * sizeof(float) <= L1_SIZE) {
    // 一次加载整个序列
    LocalTensor<float> all_input;
    DataCopy(all_input, input, time_steps * channels);

    // 后续所有 dilation 的膨胀卷积都在 L1 上做
    // 不需要 HBM 访问
    for (int t = 0; t < time_steps; t++) {
        for (int k = 0; k < kernel_size; k++) {
            int src_t = t - k * dilation;
            if (src_t < 0) continue;
            // 从 all_input 取数据(L1 hit,不是 HBM load)
            SliceAndCompute(all_input, src_t * channels, channels, k);
        }
    }
}

踩坑二:光伏预测的天气特征编码维度爆炸

光伏发电量预测的输入不只是历史发电量——还有气象数据:温度、湿度、风速、云量、辐照度。每条气象特征都可能有多个变量(数值型+分类型混合)。直接 flatten 进模型 → 输入维度爆炸。

错误:把所有气象特征直接拼到输入向量里。

# 73 个气象特征:温度×2(实际/预报)、湿度×2、风速×2、风向×16(one-hot)、
# 云量×4(one-hot)、辐照度×2、天气类型×16(one-hot)...
# 每个时间步:72(基础) + 73(气象) = 145 维
# 序列长度 72 → [72, 145] → 10440 个输入值
# LSTM 的 input_dim=10440 → W_ih [4*hidden, 10440] 爆炸

正确:气象特征单独编码(小型嵌入网络),然后和主序列合并。

// 气象特征编码器:一个小的 3 层 FC 网络
// 输入 73 维气象特征 → 输出 16 维紧凑表示
LocalTensor<float> weather_features;    // [batch, 73]
LocalTensor<float> weather_encoded;     // [batch, 16]

// 编码器只占很小的参数量:
//   layer1: 73×32 = 2336 params
//   layer2: 32×16 = 512 params
//   layer3: 16×16 = 256 params
//   总计 ≈ 3100 params(微乎其微)

FC3Layer(weather_features, weather_encoded);

// 和主序列拼接: [batch, 72, 16] merge
// 原始输入: 72 维 → 总输入: 72 + 16 = 88 维
// 而不是 72 + 73 = 145 维
MergeWithForecast(input_sequence, weather_encoded);

踩坑三:变压器预警的类别极度不平衡

变压器故障在真实数据中是极小概率事件——10000 个样本里不到 5 个故障案例。Transformer 模型在这种数据上训练,倾向输出「正常」——准确率 99.9% 但召回率 0%(所有故障都漏判)。

解决方案:用 Focal Loss 替代标准 Cross Entropy——给「难分样本」更高的 Loss 权重,压住大量「正常」样本对梯度的贡献。

// Focal Loss 在 NPU 上的 Vector 实现
__aicore__ float FocalLoss(
    LocalTensor<float>& logits,      // [batch, num_classes]
    LocalTensor<int>& labels,        // [batch]
    int batch, int num_classes,
    float gamma = 2.0                // 聚焦参数:gamma=2 代表更强的关注
) {
    // softmax
    LocalTensor<float> probs;
    Softmax(logits, probs);

    float total_loss = 0.0;
    for (int b = 0; b < batch; b++) {
        int correct_class = labels[b];
        float p_t = probs[b * num_classes + correct_class];

        // Focal Loss 核心公式:
        // FL = -(1 - p_t)^gamma * log(p_t)
        // (1 - p_t)^gamma 是调制因子
        //
        // 当 p_t 很大(样本易分,比如正常样本 p_t=0.999):
        //   (1 - 0.999)^2 = 1e-6 → Loss 被压缩到微乎其微
        //
        // 当 p_t 很小(样本难分,比如故障样本 p_t=0.05):
        //   (1 - 0.05)^2 = 0.9025 → Loss 基本不变

        float focal_weight = powf(1.0f - p_t, gamma);
        total_loss += -focal_weight * logf(p_t);
    }
    return total_loss / batch;
}

加了 Focal Loss 后,召回率从 0% 升到 87%——代价是假阳性(正常误判为故障)增加了 3%。


elec-ops-prediction 和 elec-ops-inspection 的区别:inspection 解决的是「过去有没有问题」,prediction 解决的是「未来会不会出问题」。两者的算子在仓库层面不同:inspection 依赖 ops-cv(目标检测/分割)、prediction 依赖 LSTM/TCN(时序模型)和 ops-nn(Transformer attention)。但 pipeline 的设计思路完全一致——从原始数据到业务决策,全套在 NPU 上跑完。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐