2026年8月26日

暑すぎる。

Notionのエントリーページ、ログイン状態にあることがわかりづらく、反射的に「ログイン」のつもりで「ログアウト」ボタンを押してしまうことが既に三回くらいある。大学として有料契約しているのに、「Notionを無料で始める」みたいな表示があったらログインしていないと判断しちゃうでしょ。これ、あまりUIとしてよくないと思う。

Slackの立て替え払い申請した。クレジットカードの明細は不要のはずなのに、なんか明細を待ってしまった。

さくらの立て替え払い申請もした。遅くなってしまった。来年から来たらすぐ出す。

TAの案内流した。昨年もちょうどこのあたりに流したようだ。

なんか予定をいろいろ間違えた気がする。

$ bash create_dev_ctr_macos.sh -A

Docker コンテナを起動。

docker exec --privileged -it c9f8dada416b bash

コンテナ内でMNISTを実行。

$ cd /opt/pfn/pfcomp/codegen/MLSDK/examples
$ ./exec_with_env.sh python3 mnist.py
(snip)
E0826 08:29:44.183156    19 mnlinker3.cc:106] Check `pfcpp::affinity_available()' failed!  in init at /opt/pfn/pfcomp/codegen/mnlinker/mnlinker3/mnlinker3.cc:106 with codegen_dir=/tmp/codegen_tmp_0000019/_compile_from_onnx.0/model.app.zst: Without setting appropriate affinity, MN-Core cannot perform best.

emit_codeは成功しているが、なんか失敗しているな。実機があるかどうかの判定がautoではうまくいかないようだ。

./exec_with_env.sh python3 mnist.py --device emu2

これで動いたが、極めて遅く、実時間での動作確認は難しい。PFVMのCPUバックエンドを使ってみる。

$ ./exec_with_env.sh python3 mnist.py --device pfvm:cpu
(snip)
<= compile_command end (3.913 sec)
=> send_to_others start
<= send_to_others end (0.000 sec)
<= DistributedMNCoreAppCompiler end (3.915 sec)
Correct: 9561 / 10000. Accuracy: 0.9561

動いた。PFVMとは、PyTorchで書かれた処理をONNXに変換、PFVMコンパイラが計算グラフを作り、それをPFVM Runtimeで実行する仕組み。

まず、create_dev_ctr.shをMacで実行するためには以下の修正が必要だった。

--- create_dev_ctr.sh	2026-07-27 20:53:51
+++ create_dev_ctr_macos.sh	2026-08-26 17:27:31
@@ -1,20 +1,20 @@
-#! /bin/bash
+#!/usr/bin/env bash
 set -eu -o pipefail

-MNCORE_REGEX='/dev/mnc.*'
-
 DOCKER="docker"
 IMAGE="mncore-sdk-full:0.6"
-SEMAPHORE_MOUNT="/opt/mncore_shared_semaphore:/var/tmp/mncore"
+DEFAULT_SEMAPHORE_DIR="${HOME}/.mncore_shared_semaphore"
+SEMAPHORE_MOUNT="${DEFAULT_SEMAPHORE_DIR}:/var/tmp/mncore"
+USE_DEFAULT_SEMAPHORE_MOUNT=1
 DOCKER_OPTS=()

 function usage() {
-    echo "$0 -- Launch docker container for MN-Core devenv" >&2
+    echo "$0 -- Launch docker container for MN-Core devenv on macOS" >&2
     echo "" >&2
     echo "$0 [-A] [-i IMAGE] [-s MOUNT] [-R RUNTIME_CLI] [-O DOCKER_OPTS] devices..." >&2
     echo "" >&2
     echo "Options:" >&2
-    echo "  -A              Mount all devices on the node to the container" >&2
+    echo "  -A              Use all MN-Core devices found on the host (emulator only on macOS)" >&2
     echo "  -i IMAGE[:TAG]  Use specified IMAGE (${IMAGE})" >&2
     echo "  -s MOUNT        Use specified MOUNT for sharing semaphore (${SEMAPHORE_MOUNT})" >&2
     echo "  -R RUNTIME_CLI  Container runtime CLI (${DOCKER})" >&2
@@ -27,59 +27,72 @@
 while getopts "Ai:s:R:O:h" opt; do
     case "${opt}" in
     A) USE_ALL=1;;
-    i) IMAGE=${OPTARG};;  # localhost/ prefix is required when run with -R podman, that is, the options would be -R podman -i localhost/mncore-sdk-full:0.6
-    s) SEMAPHORE_MOUNT=${OPTARG};;
+    i) IMAGE=${OPTARG};;
+    s)
+        SEMAPHORE_MOUNT=${OPTARG}
+        USE_DEFAULT_SEMAPHORE_MOUNT=0
+        ;;
     R) DOCKER=${OPTARG};;
-    O) DOCKER_OPTS+=(${OPTARG});;
-    h) usage
+    O) DOCKER_OPTS+=("${OPTARG}");;
+    h) usage;;
     esac
 done
 shift "$((OPTIND - 1))"

 DEVICES=()
 if [[ $# -ne 0 ]]; then
-    DEVICES=($@)
+    DEVICES=("$@")
 elif [[ ${USE_ALL} -ne 0 ]]; then
     echo "Enumerating devices" >&2
-    readarray -t DEVICES < <(find /dev -regex ${MNCORE_REGEX})
+    shopt -s nullglob
+    DEVICES=(/dev/mnc*)
+    shopt -u nullglob
     if [[ ${#DEVICES[@]} -eq 0 ]]; then
         echo "[WARN] No MN-Core device found. You can use emulator backend only" >&2
     fi
 else
     echo "E: No device list nor use-all flag (-A) specified." >&2
-    usage $0
+    usage
 fi

-# check if the image is already loaded to the system
-if ! ${DOCKER} image ls --format "{{.Repository}}:{{.Tag}}" | grep -q "^${IMAGE}$"; then
+if [[ ${USE_DEFAULT_SEMAPHORE_MOUNT} -ne 0 ]]; then
+    mkdir -p "${DEFAULT_SEMAPHORE_DIR}"
+fi
+
+# Check if the image is already loaded to the system.
+if ! "${DOCKER}" image ls --format "{{.Repository}}:{{.Tag}}" | grep -q "^${IMAGE}$"; then
     echo "E: Image ${IMAGE} is not found on the system."
     exit 1
 fi

+DOCKER_ARGS=(
+    run
+    --privileged
+    -d
+    -v "${SEMAPHORE_MOUNT}"
+)
+for dev in "${DEVICES[@]}"; do
+    DOCKER_ARGS+=("--device=${dev}")
+done
+DOCKER_ARGS+=("${DOCKER_OPTS[@]}" "${IMAGE}" sleep inf)
+
 echo "Starting container" >&2
-CTR_ID=$(${DOCKER} run \
-    --privileged \
-    -d \
-    -v ${SEMAPHORE_MOUNT} \
-    $(echo ${DEVICES[@]} | xargs -n1 printf " --device=%s") \
-    ${DOCKER_OPTS[@]} \
-    ${IMAGE} \
-    sleep inf)
-echo ${CTR_ID}
+CTR_ID=$("${DOCKER}" "${DOCKER_ARGS[@]}")
+echo "${CTR_ID}"

 echo "Setting up devices" >&2
-for dev in ${DEVICES[@]}; do
+for dev in "${DEVICES[@]}"; do
     echo "  ${dev}" >&2

-    if ! ${DOCKER} exec --privileged -it ${CTR_ID} gpfn3-smi config ${dev#/dev/} clock --core=750 --gddr6=15000; then
+    if ! "${DOCKER}" exec --privileged -it "${CTR_ID}" gpfn3-smi config "${dev#/dev/}" clock --core=750 --gddr6=15000; then
         echo "E: Failed to setup device ${dev}. Aborting" >&2
-        ${DOCKER} stop ${CTR_ID} >/dev/null
+        "${DOCKER}" stop "${CTR_ID}" >/dev/null
         exit 1
     fi

-    if ! ${DOCKER} exec --privileged -it ${CTR_ID} gpfn3-smi config ${dev#/dev/} mab; then
+    if ! "${DOCKER}" exec --privileged -it "${CTR_ID}" gpfn3-smi config "${dev#/dev/}" mab; then
         echo "E: Failed to setup device ${dev}. Aborting" >&2
-        ${DOCKER} stop ${CTR_ID} >/dev/null
+        "${DOCKER}" stop "${CTR_ID}" >/dev/null
         exit 1
     fi
 done

うーん、不必要にいろいろ修正してしまっているな。あとで最低限にしよう。

MN-Core Playgroundにも入ってみたが、今のところデモサイトという感じで、自分のコードをコンパイルしてどうの、という感じじゃないかな。

とりあえずCodexにhello.cを作ってもらった。

# cat hello.c
#include "mncl/device/mncore2.h"

NO_MANGLING void hello(void) {}

コンパイルする。

/opt/pfn/pfcomp/mncl/bin/mnclc hello.c -e hello -o hello.bin

-oをつけないとhello.binを吐いてくれないのでござるなぁ。

このSDKはUbuntuか。xxdを入れる。

apt update
apt-get install -y xxd

binを見る。

# xxd -l 64 hello.bin
00000000: 3000 0000 0000 0000 0000 0000 0000 0000  0...............
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0000 0000 0000 0000 0800 1000 0000 0800  ................
00000030: 0800 0000 0000 0000 0800 0000 0000 0000  ................

正しいのかわからん。CODEGEN_DUMP_VSM=1をつければVSMが出るっぽいが、適当なコードを書いても最適化で消されてしまう。

add.cを作ってみる。これが最小か?

#include "mncl/device/mncore2.h"

// z = x + y
//
// Each PE adds one f32x4x2 vector. Data is moved through the MN-Core 2
// hierarchy: DRAM -> L2BM -> L1BM -> PE, then back in reverse order.
NO_MANGLING void add(DRAM const f32* x,
                     DRAM const f32* y,
                     DRAM f32* z) {
  L2BM f32* l2bm_x = __builtin_l2bm_malloc(2048);
  L2BM f32* l2bm_y = __builtin_l2bm_malloc(2048);
  L2BM f32* l2bm_z = __builtin_l2bm_malloc(2048);

  L1BM f32* l1bm_x = __builtin_l1bm_malloc(256);
  L1BM f32* l1bm_y = __builtin_l1bm_malloc(256);
  L1BM f32* l1bm_z = __builtin_l1bm_malloc(256);

  // DRAM -> L2BM
  __builtin_dram_punicast_l2bm_0(x, l2bm_x, 128);
  __builtin_dram_punicast_l2bm_1(x + 4096, l2bm_x, 128);
  __builtin_dram_punicast_l2bm_0(y, l2bm_y, 128);
  __builtin_dram_punicast_l2bm_1(y + 4096, l2bm_y, 128);

  // L2BM -> L1BM
  for (int i = 0; i < 8; ++i) {
    __builtin_l2bm_distribute(l2bm_x + i * 512, l1bm_x + i * 64);
    __builtin_l2bm_distribute(l2bm_y + i * 512, l1bm_y + i * 64);
  }

  // L1BM -> PE
  f32x4x2 vx;
  f32x4x2 vy;
  __builtin_l1bm_distribute(l1bm_x, (void*)&vx);
  __builtin_l1bm_distribute(l1bm_y, (void*)&vy);

  // Exactly one vector addition.
  f32x4x2 vz = vx + vy;

  // PE -> L1BM -> L2BM
  __builtin_l1bm_gather((void*)&vz, l1bm_z);
  for (int i = 0; i < 8; ++i) {
    __builtin_l2bm_gather(l1bm_z + i * 64, l2bm_z + i * 512);
  }

  // L2BM -> DRAM
  __builtin_dram_punicast_upload_0(l2bm_z, z, 2048);
  __builtin_dram_punicast_upload_1(l2bm_z, z + 4096, 2048);
}
# CODEGEN_DUMP_VSM=1 /opt/pfn/pfcomp/mncl/bin/mnclc add.c -e add  2>&1 |grep fvadd
fvadd $aluf $lm72v -> $nowrite  # unknown-loc @ForwardRead3 ; unknown-loc @LMRead3

おそらく出来たっぽい。MNCoreの性質上、“Hello World"ができず、かつベクトル計算が最小だから、f32x4x2 が最小サンプルになる?

main.ccとadd.cはこうなるはず。

main.cc

#include <algorithm>
#include <cassert>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <vector>

#include "mncl/host/cl/cl.h"
#include "mncl/host/cl/constants.h"

std::vector<unsigned char> load_binary(const std::filesystem::path& path) {
  if (!std::filesystem::exists(path)) {
    throw std::runtime_error("kernel binary not found: " + path.string());
  }

  const auto size = std::filesystem::file_size(path);
  std::vector<unsigned char> data(size);
  std::ifstream input(path, std::ios::binary);
  if (!input.read(reinterpret_cast<char*>(data.data()), size)) {
    throw std::runtime_error("failed to read kernel binary: " + path.string());
  }
  return data;
}

int main() {
  constexpr size_t element_count = 4 * 4 * 16 * 8 * 2 * 4;
  constexpr size_t byte_size = element_count * sizeof(float);

  std::vector<float> x(element_count);
  std::vector<float> y(element_count);
  std::vector<float> z(element_count);
  for (size_t i = 0; i < element_count; ++i) {
    x[i] = static_cast<float>(i % 101) * 0.25f;
    y[i] = static_cast<float>(i % 97) * 0.5f;
  }

  cl_int status;
  cl_platform_id platform;
  status = clGetPlatformIDs(1, &platform, nullptr);
  assert(status == CL_SUCCESS);

  cl_device_id device;
  status = clGetDeviceIDs(platform, CL_DEVICE_TYPE_EMU_MNCORE2, 1, &device,
                          nullptr);
  assert(status == CL_SUCCESS);

  cl_context context =
      clCreateContext(nullptr, 1, &device, nullptr, nullptr, &status);
  assert(status == CL_SUCCESS);
  cl_command_queue queue = clCreateCommandQueue(context, device, 0, &status);
  assert(status == CL_SUCCESS);

  auto binary = load_binary("add.bin");
  const size_t binary_size = binary.size();
  const unsigned char* binary_data = binary.data();
  cl_program program = clCreateProgramWithBinary(
      context, 1, &device, &binary_size, &binary_data, nullptr, &status);
  assert(status == CL_SUCCESS);
  cl_kernel kernel = clCreateKernel(program, "add", &status);
  assert(status == CL_SUCCESS);

  cl_mem d_x =
      clCreateBuffer(context, CL_MEM_READ_ONLY, byte_size, nullptr, &status);
  assert(status == CL_SUCCESS);
  cl_mem d_y =
      clCreateBuffer(context, CL_MEM_READ_ONLY, byte_size, nullptr, &status);
  assert(status == CL_SUCCESS);
  cl_mem d_z =
      clCreateBuffer(context, CL_MEM_WRITE_ONLY, byte_size, nullptr, &status);
  assert(status == CL_SUCCESS);

  status = clEnqueueWriteBuffer(queue, d_x, true, 0, byte_size, x.data(), 0,
                                nullptr, nullptr);
  assert(status == CL_SUCCESS);
  status = clEnqueueWriteBuffer(queue, d_y, true, 0, byte_size, y.data(), 0,
                                nullptr, nullptr);
  assert(status == CL_SUCCESS);

  status = clSetKernelArg(kernel, 0, sizeof(cl_mem), &d_x);
  assert(status == CL_SUCCESS);
  status = clSetKernelArg(kernel, 1, sizeof(cl_mem), &d_y);
  assert(status == CL_SUCCESS);
  status = clSetKernelArg(kernel, 2, sizeof(cl_mem), &d_z);
  assert(status == CL_SUCCESS);

  cl_event event = nullptr;
  status = clEnqueueTask(queue, kernel, 0, nullptr, &event);
  assert(status == CL_SUCCESS);
  status = clEnqueueReadBuffer(queue, d_z, true, 0, byte_size, z.data(), 1,
                               &event, nullptr);
  assert(status == CL_SUCCESS);
  status = clFinish(queue);
  assert(status == CL_SUCCESS);

  float max_abs_error = 0.0f;
  for (size_t i = 0; i < element_count; ++i) {
    max_abs_error =
        std::max(max_abs_error, std::fabs(z[i] - (x[i] + y[i])));
  }

  std::cout << "x[1]=" << x[1] << ", y[1]=" << y[1]
            << ", z[1]=" << z[1] << '\n';
  std::cout << "max_abs_error=" << max_abs_error << '\n';

  clReleaseEvent(event);
  clReleaseMemObject(d_z);
  clReleaseMemObject(d_y);
  clReleaseMemObject(d_x);
  clReleaseKernel(kernel);
  clReleaseProgram(program);
  clReleaseCommandQueue(queue);
  clReleaseContext(context);

  if (max_abs_error != 0.0f) {
    std::cerr << "[FAIL] result mismatch\n";
    return 1;
  }
  std::cout << "[OK]\n";
  return 0;
}

add.c

#include "mncl/device/mncore2.h"

// z = x + y
//
// Each PE adds one f32x4x2 vector. Data is moved through the MN-Core 2
// hierarchy: DRAM -> L2BM -> L1BM -> PE, then back in reverse order.
NO_MANGLING void add(DRAM const f32* x,
                     DRAM const f32* y,
                     DRAM f32* z) {
  L2BM f32* l2bm_x = __builtin_l2bm_malloc(2048);
  L2BM f32* l2bm_y = __builtin_l2bm_malloc(2048);
  L2BM f32* l2bm_z = __builtin_l2bm_malloc(2048);

  L1BM f32* l1bm_x = __builtin_l1bm_malloc(256);
  L1BM f32* l1bm_y = __builtin_l1bm_malloc(256);
  L1BM f32* l1bm_z = __builtin_l1bm_malloc(256);

  // DRAM -> L2BM
  __builtin_dram_punicast_l2bm_0(x, l2bm_x, 128);
  __builtin_dram_punicast_l2bm_1(x + 4096, l2bm_x, 128);
  __builtin_dram_punicast_l2bm_0(y, l2bm_y, 128);
  __builtin_dram_punicast_l2bm_1(y + 4096, l2bm_y, 128);

  // L2BM -> L1BM
  for (int i = 0; i < 8; ++i) {
    __builtin_l2bm_distribute(l2bm_x + i * 512, l1bm_x + i * 64);
    __builtin_l2bm_distribute(l2bm_y + i * 512, l1bm_y + i * 64);
  }

  // L1BM -> PE
  f32x4x2 vx;
  f32x4x2 vy;
  __builtin_l1bm_distribute(l1bm_x, (void*)&vx);
  __builtin_l1bm_distribute(l1bm_y, (void*)&vy);

  // Exactly one vector addition.
  f32x4x2 vz = vx + vy;

  // PE -> L1BM -> L2BM
  __builtin_l1bm_gather((void*)&vz, l1bm_z);
  for (int i = 0; i < 8; ++i) {
    __builtin_l2bm_gather(l1bm_z + i * 64, l2bm_z + i * 512);
  }

  // L2BM -> DRAM
  __builtin_dram_punicast_upload_0(l2bm_z, z, 2048);
  __builtin_dram_punicast_upload_1(l2bm_z, z + 4096, 2048);
}
$ /opt/pfn/pfcomp/mncl/bin/mnclc add.c -e add -o add.bin
$ c++ -std=c++23 main.cc -o add_host $(pkg-config --cflags --libs mncl)
$ ./add_host
x[1]=0.25, y[1]=0.5, z[1]=0.75
max_abs_error=0
[OK]

できたっぽい。うーん、やっぱりOpenCL系の構文はしんどいなぁ・・・

いくつかハマりポイントがあって、MacのデフォルトのBashが古いのでreadarrayが使えない(Homebrewでbashを入れる必要がある)、Dockerイメージが要求する場所が読めないのでそこを修正する必要がある、mnclcのドキュメントがほとんどなく、何をどうしてよいかわからない、あたりか。

あとで「MNCore SDKを使ってみる」的な記事にまとめるか・・・