Apache SINGA 构建并运行示例应用指南

Apache SINGA 构建并运行示例应用指南:基于 5.1.0 编写并运行 MNIST 多层感知机(model.Model 图模式)与官方 train_cnn.py 卷积网络示例,含数据下载、CPU 适配、完整源码、运行命令与真实回显、模型保存恢复。

Apache SINGAApache SINGA2026/10/12

Apache SINGA 构建并运行示例应用指南

目标:从零构建并运行两个基于 Apache SINGA 5.1.0 的训练示例:MNIST 多层感知机(model.Model 图模式,CPU)与官方 train_cnn.py 卷积网络示例。给出完整源码、数据准备、运行命令与真实回显:MLP 训练 2 个 epoch 后评估准确率 0.937901,CNN 训练 1 个 epoch 后评估准确率 0.942308。所有命令与输出均在 Ubuntu 22.04.5 + Python 3.10.12 + SINGA 5.1.0(CPU + oneDNN)环境实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS
  • 架构:x86_64(12 核,内核 6.8.0-138-generic)
  • Python:3.10.12(虚拟环境 /root/singa-venv)
  • SINGA 版本:5.1.0(源码编译,CPU + oneDNN 1.1,安装方法见《Apache SINGA 安装指南》)
  • numpy:1.23.5(SINGA 5.1.0 兼容上限,见《Apache SINGA 安装指南》FAQ)
  • Python 依赖:protobuf 3.20.3、deprecated、future、pillow(CNN 示例需要)
  • 示例目录:MLP /root/singa-examples/mlp/,CNN /root/singa-examples/cnn/
  • 数据目录:MNIST npz /tmp/mnist.npz,MNIST IDX /tmp/*.gz

二、前置条件

2.1 验证 SINGA 可用

source /root/singa-venv/bin/activate
python3 -c 'import singa; print(singa.__version__)'

实测回显:

5100

2.2 安装示例依赖

CNN 示例的 train_cnn.py 依赖 PIL(pillow):

pip install pillow

三、SINGA 示例代码结构

SINGA 5.1.0 源码树 examples/ 目录内置多个示例(本指南将用到 mlp 与 cnn 的代码结构):

示例目录内容说明
mlp/model.pyMLP(model.Model)官方 MLP 模型类(默认 CUDA 设备,本指南改写为 CPU)
cnn/train_cnn.py训练入口脚本支持 cnn/resnet/xceptionnet/mlp/alexnet × mnist/cifar10/cifar100
cnn/model/cnn.pyCNN(model.Model)官方 CNN 模型类
cnn/data/mnist.pyMNIST 加载器读取 /tmp 下的 IDX gz 文件

推荐在用户目录组织自己的示例:

mkdir -p /root/singa-examples/mlp

四、示例一:MNIST 多层感知机(CPU 图模式)

4.1 模型与训练脚本

创建 /root/singa-examples/mlp/mlp.py。模型类继承 model.Model(与官方 examples/mlp/model.py 同构),训练循环使用图模式(use_graph=True,官方示例的稳定路径);数据自动下载 mnist.npz:

import argparse
import os
import time
import urllib.request

import numpy as np

from singa import device
from singa import layer
from singa import model
from singa import opt
from singa import tensor

MNIST_URL = 'https://s3.amazonaws.com/img-datasets/mnist.npz'
MNIST_PATH = '/tmp/mnist.npz'


def load_mnist(path=MNIST_PATH):
    if not os.path.exists(path):
        print('Downloading %s to %s ...' % (MNIST_URL, path))
        urllib.request.urlretrieve(MNIST_URL, path)
    data = np.load(path)
    train_x = data['x_train'].reshape(-1, 784).astype(np.float32) / 255
    train_y = data['y_train'].astype(np.int32)
    test_x = data['x_test'].reshape(-1, 784).astype(np.float32) / 255
    test_y = data['y_test'].astype(np.int32)
    return train_x, train_y, test_x, test_y


class MLP(model.Model):

    def __init__(self, data_size=784, perceptron_size=100, num_classes=10):
        super(MLP, self).__init__()
        self.num_classes = num_classes
        self.dimension = 2

        self.relu = layer.ReLU()
        self.linear1 = layer.Linear(perceptron_size)
        self.linear2 = layer.Linear(num_classes)
        self.softmax_cross_entropy = layer.SoftMaxCrossEntropy()

    def forward(self, inputs):
        y = self.linear1(inputs)
        y = self.relu(y)
        y = self.linear2(y)
        return y

    def train_one_batch(self, x, y, dist_option='plain', spars=None):
        out = self.forward(x)
        loss = self.softmax_cross_entropy(out, y)
        if dist_option == 'plain':
            self.optimizer(loss)
        elif dist_option == 'half':
            self.optimizer.backward_and_update_half(loss)
        elif dist_option == 'partialUpdate':
            self.optimizer.backward_and_partial_update(loss)
        return out, loss


def accuracy(pred, target):
    y = np.argmax(pred, axis=1)
    return np.array(y == target, 'int').sum()


def train(max_epoch, batch_size, hidden_size, lr):
    train_x, train_y, test_x, test_y = load_mnist()

    dev = device.get_default_device()
    sgd = opt.SGD(lr=lr, momentum=0.9, weight_decay=1e-5)
    mlp = MLP(data_size=train_x.shape[1],
              perceptron_size=hidden_size,
              num_classes=10)

    tx = tensor.Tensor((batch_size, train_x.shape[1]), dev, tensor.float32)
    ty = tensor.Tensor((batch_size,), dev, tensor.int32)
    mlp.optimizer = sgd
    mlp.compile([tx], is_train=True, use_graph=True, sequential=True)
    mlp.train()

    num_train_batch = train_x.shape[0] // batch_size
    num_test_batch = test_x.shape[0] // batch_size
    idx = np.arange(train_x.shape[0], dtype=np.int32)

    for epoch in range(max_epoch):
        start_time = time.time()
        np.random.shuffle(idx)
        print('Starting Epoch %d:' % epoch)

        train_correct = np.zeros(shape=[1], dtype=np.float32)
        train_loss = np.zeros(shape=[1], dtype=np.float32)
        for b in range(num_train_batch):
            x = train_x[idx[b * batch_size:(b + 1) * batch_size]]
            y = train_y[idx[b * batch_size:(b + 1) * batch_size]]
            tx.copy_from_numpy(x)
            ty.copy_from_numpy(y)
            out, loss = mlp(tx, ty, 'plain', spars=None)
            train_correct += accuracy(tensor.to_numpy(out), y)
            train_loss += tensor.to_numpy(loss)[0]

        print('Training loss = %f, training accuracy = %f' %
              (train_loss, train_correct / (num_train_batch * batch_size)),
              flush=True)

        mlp.eval()
        test_correct = np.zeros(shape=[1], dtype=np.float32)
        for b in range(num_test_batch):
            x = test_x[b * batch_size:(b + 1) * batch_size]
            y = test_y[b * batch_size:(b + 1) * batch_size]
            tx.copy_from_numpy(x)
            ty.copy_from_numpy(y)
            out_test = mlp(tx)
            test_correct += accuracy(tensor.to_numpy(out_test), y)

        print('Evaluation accuracy = %f, Elapsed Time = %fs' %
              (test_correct / (num_test_batch * batch_size),
               time.time() - start_time),
              flush=True)
        mlp.train()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-m', '--max-epoch', default=2, type=int,
                        help='maximum epochs', dest='max_epoch')
    parser.add_argument('-b', '--batch-size', default=128, type=int,
                        help='batch size', dest='batch_size')
    parser.add_argument('-l', '--learning-rate', default=0.01, type=float,
                        help='learning rate', dest='lr')
    parser.add_argument('-H', '--hidden-size', default=100, type=int,
                        help='hidden perceptron size', dest='hidden_size')
    args = parser.parse_args()
    np.random.seed(0)
    train(args.max_epoch, args.batch_size, args.hidden_size, args.lr)

命令行帮助(实测):

$ python3 mlp.py --help
usage: mlp.py [-h] [-m MAX_EPOCH] [-b BATCH_SIZE] [-l LR] [-H HIDDEN_SIZE]

options:
  -h, --help            show this help message and exit
  -m MAX_EPOCH, --max-epoch MAX_EPOCH
                        maximum epochs
  -b BATCH_SIZE, --batch-size BATCH_SIZE
                        batch size
  -l LR, --learning-rate LR
                        learning rate
  -H HIDDEN_SIZE, --hidden-size HIDDEN_SIZE
                        hidden perceptron size

4.2 运行

cd /root/singa-examples/mlp
python3 mlp.py -m 2

实测回显(exit=0):

Downloading https://s3.amazonaws.com/img-datasets/mnist.npz to /tmp/mnist.npz ...
Starting Epoch 0:
Training loss = 230.104675, training accuracy = 0.863498
Evaluation accuracy = 0.920873, Elapsed Time = 17.119804s
Starting Epoch 1:
Training loss = 124.375572, training accuracy = 0.925664
Evaluation accuracy = 0.937901, Elapsed Time = 25.127998s

4.3 结果解读

  • 数据:首次运行自动从 https://s3.amazonaws.com/img-datasets/mnist.npz 下载(约 11 MB)到 /tmp/mnist.npz;已有文件时跳过下载,故重跑时无第一行 Downloading。
  • 训练:Training loss 为 469 个 batch 的损失累加值,training accuracy 为训练集批次准确率(非全量),epoch 1(0.925664)高于 epoch 0(0.863498),损失从 230.10 降到 124.38,模型正常收敛。
  • 评估:每个 epoch 结束后在 1 万张测试图上评估,2 个 epoch 后评估准确率 0.937901,符合 100 隐层单元 MLP 在 MNIST 上的正常水平。
  • 耗时:12 核 CPU 上单 epoch 约 17~25 秒;耗时随 batch size、hidden size 与核数变化。

五、示例二:MNIST 卷积网络(官方 train_cnn.py)

5.1 准备 MNIST IDX 数据

官方 cnn/data/mnist.py 从 /tmp 读取 4 个 IDX gz 文件(train-images-idx3-ubyte.gz、train-labels-idx1-ubyte.gz、t10k-images-idx3-ubyte.gz、t10k-labels-idx1-ubyte.gz)。官方下载脚本中的 http://yann.lecun.com/exdb/mnist/ 源已失效(实测返回 404),改用可用镜像:

cd /tmp
for f in train-images-idx3-ubyte.gz train-labels-idx1-ubyte.gz \
         t10k-images-idx3-ubyte.gz t10k-labels-idx1-ubyte.gz; do
  curl -sO https://ossci-datasets.s3.amazonaws.com/mnist/$f
done
ls -la /tmp/*.gz

实测回显:

-rw-r--r-- 1 root root 1648877 ... /tmp/t10k-images-idx3-ubyte.gz
-rw-r--r-- 1 root root    4542 ... /tmp/t10k-labels-idx1-ubyte.gz
-rw-r--r-- 1 root root 9912422 ... /tmp/train-images-idx3-ubyte.gz
-rw-r--r-- 1 root root   28881 ... /tmp/train-labels-idx1-ubyte.gz

5.2 复制官方示例并适配 CPU 设备

把官方 train_cnn.py、model/、data/ 复制到用户示例目录:

mkdir -p /root/singa-examples/cnn
cp /opt/singa/examples/cnn/train_cnn.py /root/singa-examples/cnn/
cp -r /opt/singa/examples/cnn/model /opt/singa/examples/cnn/data /root/singa-examples/cnn/

train_cnn.py 中设备行(约第 110 行)默认创建 CUDA 设备,源码注释明确说明 CPU 机器需修改:

dev = device.create_cuda_gpu_on(local_rank)  # need to change to CPU device for CPU-only machines

按注释改为 CPU 设备:

dev = device.get_default_device()  # CPU device for CPU-only machines

模型类(model/cnn.py)中的 layer.Conv2d 在 5.1.0 中使用「滤波器数在前」的签名,输入通道由输入张量自动推断,两层卷积为:

self.conv1 = layer.Conv2d(20, 5, padding=0, activation="RELU")   # 20 个 5x5 滤波器
self.conv2 = layer.Conv2d(50, 5, padding=0, activation="RELU")   # 50 个 5x5 滤波器

5.3 运行

cd /root/singa-examples/cnn
python3 train_cnn.py cnn mnist -m 1 -b 64

实测回显(exit=0):

Starting Epoch 0:
Training loss = 582.897705, training accuracy = 0.795224
Evaluation accuracy = 0.942308, Elapsed Time = 242.777421s

命令行帮助(实测节选):

$ python3 train_cnn.py --help
usage: train_cnn.py [-h] [-p {float32,float16}] [-m MAX_EPOCH] [-b BATCH_SIZE]
                    [-l LR] [-i DEVICE_ID] [-g] [-v VERBOSITY]
                    {cnn,resnet,xceptionnet,mlp,alexnet}
                    {mnist,cifar10,cifar100}

5.4 结果解读

  • 位置参数:cnn 选模型、mnist 选数据集;-m 1 只训练 1 个 epoch,-b 64 为批大小。
  • 训练:938 个 batch 后训练批次准确率 0.795224;评估:测试集准确率 0.942308——单 epoch 即达到 94%,卷积结构明显优于 MLP 同期水平。
  • 耗时:CPU 上卷积算子开销大(242.8 秒/epoch),生产训练建议 GPU;验证功能正确性时可用 -m 1 -b 64 最小迭代。

六、模型保存与恢复

model.Model 提供检查点接口,实测保存与恢复:

import numpy as np, os
from singa import tensor, device, layer, opt, model, autograd

class MLP(model.Model):
    def __init__(self):
        super().__init__()
        self.linear1 = layer.Linear(8)
        self.linear2 = layer.Linear(4)
    def forward(self, x):
        return self.linear2(self.linear1(x))
    def train_one_batch(self, x, y, dist_option='plain', spars=None):
        out = self.forward(x)
        loss = autograd.softmax_cross_entropy(out, y)
        self.optimizer(loss)
        return out, loss

dev = device.get_default_device()
m = MLP()
tx = tensor.Tensor((2, 10), dev, tensor.float32)
ty = tensor.Tensor((2,), dev, tensor.int32)
m.optimizer = opt.SGD(lr=0.01)
m.compile([tx], is_train=True, use_graph=True, sequential=True)
m.train()
tx.copy_from_numpy(np.random.rand(2, 10).astype(np.float32))
ty.copy_from_numpy(np.array([1, 2], dtype=np.int32))
out, loss = m(tx, ty, 'plain', spars=None)
print('train step loss:', float(tensor.to_numpy(loss)[0]))
os.makedirs('/tmp/singa-ckpt', exist_ok=True)
epoch = tensor.Tensor((1,), dev, tensor.int32)
epoch.set_value(1)
m.save_states('/tmp/singa-ckpt/mlp-states', aux_states={'epoch': epoch})
print('ckpt files:', sorted(os.listdir('/tmp/singa-ckpt')))
m2 = MLP()
m2.optimizer = opt.SGD(lr=0.01)
m2.compile([tx], is_train=True, use_graph=True, sequential=True)
m2.load_states('/tmp/singa-ckpt/mlp-states')
print('load_states OK')

实测回显:

train step loss: 1.0017001628875732
ckpt files: ['mlp-states']
load_states OK

⚠️ 注意:save_states 的 aux_states 中每个 Tensor 必须先 set_value(...) 初始化,否则进程直接 abort(Check failed: initialized_ Must initialize data before reading it)。检查点为单个文件(内部 zip 结构),恢复端 load_states(fpath) 读取同名文件。

七、注意事项

⚠️ 注意:

  1. 训练一律使用 model.Model + compile(..., use_graph=True) 图模式(官方示例的稳定路径);手写 autograd.backward + 优化器逐参更新的非图循环在 CPU 上存在崩溃风险。
  2. optimizer 通过 mlp.optimizer = sgd 赋值(5.1.0 的 Model 无 set_optimizer 方法名,官方示例中的同名调用以属性赋值等价实现)。
  3. 训练与评估切换:mlp.train() / mlp.eval();评估阶段不更新参数。
  4. numpy 必须为 1.23.x(np.float 移除问题),Python 3.10 需先按《Apache SINGA 安装指南》完成 model.py 兼容性补丁。
  5. GPU 训练:device.create_cuda_gpu() 等接口需 CUDA/cuDNN 构建支持;本文全部示例按 CPU 环境实测。

八、常见问题

Q1:运行示例报 cannot import name 'Iterable' from 'collections'? 未打 Python 3.10 兼容性补丁。按《Apache SINGA 安装指南》4.7 节修补 site-packages/singa/model.py 后重试。

Q2:python3 mlp.py 没有下载提示,直接开始训练? /tmp/mnist.npz 已存在时跳过下载(脚本逻辑如此);删除该文件重跑可复现 Downloading 输出。

Q3:CNN 示例下载 MNIST 报 404? 官方脚本内 yann.lecun.com 源已失效(实测 HTTP 404)。按 5.1 节改用 https://ossci-datasets.s3.amazonaws.com/mnist/ 镜像下载 4 个 IDX 文件到 /tmp。

Q4:CNN 示例报 weights shape should not change 后 abort? SINGA 构建未启用 oneDNN(USE_DNNL=OFF)时 CPU 卷积会触发该断言。按《Apache SINGA 安装指南》4.2/4.4 节安装 oneDNN 1.1 并以 -DUSE_DNNL=ON 重新编译安装。

Q5:评估准确率每次运行不一致? 训练循环以 np.random.seed(0) 固定种子且 shuffle 顺序确定,实测两次运行 loss/准确率完全一致;若自行改动批大小、线程数或数据顺序,结果会有差异。

九、参考链接

  • Apache SINGA 官网:https://singa.apache.org/
  • MLP 教程:https://singa.apache.org/docs/mlp/
  • CNN 示例说明:https://singa.apache.org/docs/cnn/
  • SINGA 源码 examples 目录:https://github.com/apache/singa/tree/master/examples
  • MNIST 数据集镜像:https://ossci-datasets.s3.amazonaws.com/mnist/