読者です 読者をやめる 読者になる 読者になる

node-ffi で OpenCL を使う2 - 演算の実行

Node.js JavaScript FFI OpenCL

node-ffi で OpenCL を使う」 に続き、Node.js を使って OpenCL の演算を実施してみます。

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20160725/

はじめに

演算の実行には ref-array モジュールを使った方が便利なため、node-ffi をインストールした環境へ追加でインストールしておきます。

ref-array インストール例
> npm install ref-array

OpenCL の演算実行サンプル

今回は配列の要素を 3乗する OpenCL のコード(以下)を Node.js から実行する事にします。

cube.cl
__kernel void cube(
    __global float* input,
    __global float* output,
    const unsigned int count)
{
    int i = get_global_id(0);

    if (i < count) {
        output[i] = input[i] * input[i] * input[i];
    }
}

サンプルコード概要

上記 cube.cl を実行する Node.js サンプルコードの全体像です。(OpenCLAPI は try-finally 内で呼び出しています)

OpenCL 演算の入力値として data 変数の値を使用します。OpenCL のコードはファイルから読み込んで code 変数へ設定しています。

OpenCL APIclCreateXXX で作成したリソースは clReleaseXXX で解放するようなので、解放処理を都度 releaseList へ追加しておき、finally で実行するようにしています。

なお、OpenCL API のエラーコード取得には以下の 2通りがあります。(使用する関数による)

  • 関数の戻り値でエラーコードを取得
  • 関数の引数(ポインタ)でエラーコードを取得
calc.js (全体)
'use strict';

const fs = require('fs');
const ref = require('ref');
const ArrayType = require('ref-array');
const ffi = require('ffi');

const CL_DEVICE_TYPE_DEFAULT = 1;

const CL_MEM_READ_WRITE = (1 << 0);
const CL_MEM_WRITE_ONLY = (1 << 1);
const CL_MEM_READ_ONLY = (1 << 2);
const CL_MEM_USE_HOST_PTR = (1 << 3);
const CL_MEM_ALLOC_HOST_PTR = (1 << 4);
const CL_MEM_COPY_HOST_PTR = (1 << 5);

const intPtr = ref.refType(ref.types.int32);
const uintPtr = ref.refType(ref.types.uint32);
const sizeTPtr = ref.refType('size_t');
const StringArray = ArrayType('string');

const clLib = (process.platform == 'win32') ? 'OpenCL' : 'libOpenCL';

// 使用する OpenCL の関数定義
const openCl = ffi.Library(clLib, {
    'clGetPlatformIDs': ['int', ['uint', sizeTPtr, uintPtr]],
    'clGetDeviceIDs': ['int', ['size_t', 'ulong', 'uint', sizeTPtr, uintPtr]],
    'clCreateContext': ['pointer', ['pointer', 'uint', sizeTPtr, 'pointer', 'pointer', intPtr]],
    'clReleaseContext': ['int', ['pointer']],
    'clCreateProgramWithSource': ['pointer', ['pointer', 'uint', StringArray, sizeTPtr, intPtr]],
    'clBuildProgram': ['int', ['pointer', 'uint', sizeTPtr, 'string', 'pointer', 'pointer']],
    'clReleaseProgram': ['int', ['pointer']],
    'clCreateKernel': ['pointer', ['pointer', 'string', intPtr]],
    'clReleaseKernel': ['int', ['pointer']],
    'clCreateBuffer': ['pointer', ['pointer', 'ulong', 'size_t', 'pointer', intPtr]],
    'clReleaseMemObject': ['int', ['pointer']],
    'clSetKernelArg': ['int', ['pointer', 'uint', 'size_t', 'pointer']],
    'clCreateCommandQueue': ['pointer', ['pointer', 'size_t', 'ulong', intPtr]],
    'clReleaseCommandQueue': ['int', ['pointer']],
    'clEnqueueReadBuffer': ['int', ['pointer', 'pointer', 'bool', 'size_t', 'size_t', 'pointer', 'uint', 'pointer', 'pointer']],
    'clEnqueueNDRangeKernel': ['int', ['pointer', 'pointer', 'uint', sizeTPtr, sizeTPtr, sizeTPtr, 'uint', 'pointer', 'pointer']]
});

// エラーチェック
const checkError = (err, title = '') => {
    if (err instanceof Buffer) {
        // ポインタの場合はエラーコードを取り出す
        err = intPtr.get(err);
    }

    if (err != 0) {
        throw new Error(`${title} Error: ${err}`);
    }
};

// 演算対象データ
const data = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9];

const functionName = process.argv[2]
// OpenCL コードの読み込み(ファイルから)
const code = fs.readFileSync(process.argv[3]);

const releaseList = [];

try {
    ・・・ OpenCL API の呼び出し処理 ・・・

} finally {
    // リソースの解放
    releaseList.reverse().forEach( f => f() );
}

clCreateProgramWithSourceOpenCL のコードを渡す際に、ref-array で作成した String の配列 StringArray を使っています。

OpenCL 処理部分

今回は以下のような OpenCL API を使っています。

番号 概要 OpenCL 関数名
(1) プラットフォーム取得 clGetPlatformIDs
(2) バイス取得 clGetDeviceIDs
(3) コンテキスト作成 clCreateContext
(4) コマンドキュー作成 clCreateCommandQueue
(5) プログラム作成 clCreateProgramWithSource
(6) プログラムのビルド clBuildProgram
(7) カーネル作成 clCreateKernel
(8) 引数用のバッファ作成 clCreateBuffer
(9) 引数の設定 clSetKernelArg
(10) 処理の実行 clEnqueueNDRangeKernel
(11) 結果の取得 clEnqueueReadBuffer

OpenCL のコードを実行するには (6) のように API を使ってビルドする必要があります。

Node.js と OpenCL 間で配列データ等をやりとりするには (8) で作ったバッファを使います。(入力値をバッファへ書き込んで、出力値をバッファから読み出す)

また、今回は clEnqueueNDRangeKernel を使って実行しましたが、clEnqueueTask を使って実行する方法もあります。

calc.js (OpenCL 処理部分)
・・・
try {
    const platformIdsPtr = ref.alloc(sizeTPtr);
    // (1) プラットフォーム取得
    let res = openCl.clGetPlatformIDs(1, platformIdsPtr, null);

    checkError(res, 'clGetPlatformIDs');

    const platformId = sizeTPtr.get(platformIdsPtr);

    const deviceIdsPtr = ref.alloc(sizeTPtr);
    // (2) デバイス取得 (デフォルトをとりあえず使用)
    res = openCl.clGetDeviceIDs(platformId, CL_DEVICE_TYPE_DEFAULT, 1, deviceIdsPtr, null);

    checkError(res, 'clGetDeviceIDs');

    const deviceId = sizeTPtr.get(deviceIdsPtr);

    const errPtr = ref.alloc(intPtr);
    // (3) コンテキスト作成
    const ctx = openCl.clCreateContext(null, 1, deviceIdsPtr, null, null, errPtr);

    checkError(errPtr, 'clCreateContext');
    releaseList.push( () => openCl.clReleaseContext(ctx) );
    // (4) コマンドキュー作成
    const queue = openCl.clCreateCommandQueue(ctx, deviceId, 0, errPtr);

    checkError(errPtr, 'clCreateCommandQueue');
    releaseList.push( () => openCl.clReleaseCommandQueue(queue) );

    const codeArray = new StringArray([code.toString()]);
    // (5) プログラム作成
    const program = openCl.clCreateProgramWithSource(ctx, 1, codeArray, null, errPtr);

    checkError(errPtr, 'clCreateProgramWithSource');
    releaseList.push( () => openCl.clReleaseProgram(program) );
    // (6) プログラムのビルド
    res = openCl.clBuildProgram(program, 1, deviceIdsPtr, null, null, null)

    checkError(res, 'clBuildProgram');
    // (7) カーネル作成
    const kernel = openCl.clCreateKernel(program, functionName, errPtr);

    checkError(errPtr, 'clCreateKernel');
    releaseList.push( () => openCl.clReleaseKernel(kernel) );

    const FixedFloatArray = ArrayType('float', data.length);
    // 入力データ
    const inputData = new FixedFloatArray(data);

    const bufSize = inputData.buffer.length;
    // (8) 引数用のバッファ作成(入力用)し inputData の内容を書き込む
    const inClBuf = openCl.clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, bufSize, inputData.buffer, errPtr);

    checkError(errPtr, 'clCreateBuffer In');
    releaseList.push( () => openCl.clReleaseMemObject(inClBuf) );

    // (8) 引数用のバッファ作成(出力用)
    const outClBuf = openCl.clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, bufSize, null, errPtr);

    checkError(errPtr, 'clCreateBuffer Out');
    releaseList.push( () => openCl.clReleaseMemObject(outClBuf) );

    const inClBufRef = inClBuf.ref();
    // (9) 引数の設定
    res = openCl.clSetKernelArg(kernel, 0, inClBufRef.length, inClBufRef);

    checkError(res, 'clSetKernelArg 0');

    const outClBufRef = outClBuf.ref();
    // (9) 引数の設定
    res = openCl.clSetKernelArg(kernel, 1, outClBufRef.length, outClBufRef);

    checkError(res, 'clSetKernelArg 1');

    const ct = ref.alloc(ref.types.uint32, data.length);

    // (9) 引数の設定
    res = openCl.clSetKernelArg(kernel, 2, ct.length, ct);

    checkError(res, 'clSetKernelArg 2');

    const globalPtr = ref.alloc(sizeTPtr);
    sizeTPtr.set(globalPtr, 0, data.length);
    // (10) 処理の実行
    res = openCl.clEnqueueNDRangeKernel(queue, kernel, 1, null, globalPtr, null, 0, null, null);

    checkError(res, 'clEnqueueNDRangeKernel');

    const resData = new FixedFloatArray();

    // (11) 結果の取得 (outClBuf の内容を resData へ)
    res = openCl.clEnqueueReadBuffer(queue, outClBuf, true, 0, resData.buffer.length, resData.buffer, 0, null, null);

    checkError(res, 'clEnqueueReadBuffer');

    // 結果出力
    for (let i = 0; i < resData.length; i++) {
        console.log(resData[i]);
    }

} finally {
    // リソースの解放
    releaseList.reverse().forEach( f => f() );
}

動作確認

今回は以下の Node.js を使って WindowsLinux の両方で動作確認します。

  • Node.js v6.3.1

(a) Windows で実行

node-ffi で OpenCL を使う」 で構築した環境へ ref-array をインストールして実行しました。

実行結果 (Windows
> node calc.js cube cube.cl

1.3310000896453857
10.648000717163086
35.93699645996094
85.18400573730469
166.375
287.4959716796875
456.532958984375
681.4720458984375
970.2988891601562

(b) Linux で実行

前回 の Docker イメージを使って実行します。

calc.js と cube.cl を /vagrant/work へ配置し、Docker コンテナからは /work でアクセスできるようにマッピングしました。

Docker コンテナ実行
$ docker run --rm -it -v /vagrant/work:/work sample/opencl:0.1 bash
Node.js と必要なモジュールのインストール
# curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.2/install.sh | bash
・・・
# source ~/.bashrc

# nvm install v6.3.1
・・・
# npm install -g node-gyp
・・・
# cd /work
# npm install ffi ref-array
・・・

/work 内で実行します。

実行結果 (Linux
# node calc.js cube cube.cl

1.3310000896453857
10.648000717163086
35.93699645996094
85.18400573730469
166.375
287.4959716796875
456.532958984375
681.4720458984375
970.2988891601562

Docker で OpenCL の実行環境を構築 - pocl

Docker OpenCL Node.js

前回の 「node-ffi で OpenCL を使う」 では Windows 上で Node.js から OpenCLAPI を呼び出してみましたが、これを Linux 上で行うために OpenCL の実行環境を Docker で構築してみました。

OpenCL の実行環境として pocl を使い、LinuxFedora を使います。(Fedora にしたのは pocl のインストールが簡単だったから)

サンプルのソースは http://github.com/fits/try_samples/tree/master/blog/20160711/

Docker イメージ作成

Docker の公式 fedora イメージを使って OpenCL の Docker イメージを作成します。

最近の Fedora ではパッケージ管理に DNF (yum の後継) を使うようなので、dnf コマンドを使った Dockerfile を作成しました。

Dockerfile
FROM fedora

RUN dnf update -y
RUN dnf install -y clinfo pocl pocl-devel

RUN ln -s /lib64/libpoclu.so /lib64/libOpenCL.so

RUN dnf install -y tar findutils make python2

RUN dnf clean all

OpenCL を実行するだけなら pocl のインストールだけで良さそうですが、ついでに clinfo と pocl-devel もインストールしています。 (clinfo で OpenCL の環境情報を参照できます)

なお、OpenCL の標準的なライブラリ名は libOpenCL.so のようですが、pocl をインストールしても libOpenCL.so を作ってくれなかったので、libpoclu.soシンボリックリンクとして作成するようにしています。※

 ※ node-ffi で指定するライブラリ名を libpoclu とするなら、
    libOpenCL.so は無くてもよい

また、tar と findutils は nvm で、make と python2 は node-gyp で必要となるため、ついでにインストールしています。

Docker イメージは docker build コマンドで作成します。

ビルド例 (Docker イメージ作成)
$ docker build --no-cache -t sample/opencl:0.1 .

OpenCL 動作確認

まずは、今回の環境で動作するように 前回 のサンプルを少し書き換えます。 ライブラリ名をプラットフォームに合わせて切り替えるようにします。

/vagrant/work/platform_info.js (変更点)
・・・
const clLib = (process.platform == 'win32') ? 'OpenCL' : 'libOpenCL';

const openCl = ffi.Library(clLib, {
    ・・・
});
・・・

先程作成した Docker イメージを使ってコンテナを起動します。

Docker コンテナ実行
$ docker run --rm -it -v /vagrant/work:/work sample/opencl:0.1 bash

ここからは Docker コンテナ内での操作となります。

dnf (もしくは yum) だと古いバージョンの Node.js をインストールしてしまうので、nvm を使って Node.js v6.2.2 をインストールします。

Node.js のインストール後、node-gyp と node-ffi を npm でそれぞれインストールします。

Node.js と必要なモジュールのインストール
# curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.2/install.sh | bash
・・・
# source ~/.bashrc

# nvm install v6.2.2
・・・
# npm install -g node-gyp
・・・
# cd /work
# npm install ffi
・・・

最後に platform_info.js を実行すると問題なく動作しました。

platform_info.js 実行
# node platform_info.js

FULL_PROFILE
OpenCL 2.0 pocl 0.13, LLVM 3.8.0
Portable Computing Language

node-ffi で OpenCL を使う

Node.js JavaScript FFI OpenCL

Windows 環境で node-ffi (Node.js Foreign Function Interface) を使って OpenCLAPI を呼び出してみました。

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20160627/

なお、OpenCL 上での演算は今回扱いませんが、単純な演算のサンプルは ここ に置いてます。

はじめに

node-ffi のインストール

まずは、node-gyp をインストールしておきます。 node-gyp を Windows 環境で使うには VC++Python 2.7 が必要です。

node-gyp インストール例
> npm install -g node-gyp

node-ffi をインストールします。(モジュール名は node-ffi ではなく ffi です)

node-ffi インストール例
> npm install ffi

node-ffi の使い方

node-ffi では Library 関数を使ってネイティブライブラリの関数をマッピングします。

ffi.Library(<ライブラリ名>, {
    <関数名>: [<戻り値の型>, [<第1引数の型>, <第2引数の型>, ・・・]],
    ・・・
})

引数の型などはライブラリのヘッダーファイルなどを参考にして設定します。

例えば、OpenCL.dll (Windows 環境の場合) の clGetPlatformIDs 関数を Node.js から openCl.clGetPlatformIDs(・・・) で呼び出すには以下のようにします。

Library の使用例
const openCl = ffi.Library('OpenCL', {
    'clGetPlatformIDs': ['int', ['uint', sizeTPtr, uintPtr]],
    ・・・
});

ref モジュールの refType でポインタ用の型を定義する事が可能です。

refType の使用例
const uintPtr = ref.refType(ref.types.uint32);
const sizeTPtr = ref.refType('size_t');

OpenCL の利用

それでは、下記 OpenCL ランタイムをインストールした Windows 環境で、OpenCLAPI を 3つほど呼び出してみます。

1. OpenCL のデバイスID取得

まずは、以下を実施してみます。

  • (1) clGetPlatformIDs を使ってプラットフォームIDを取得
  • (2) clGetDeviceIDs を使ってデバイスIDを取得

OpenCL (v1.2) のヘッダーファイルを見てみると、プラットフォームIDの型 cl_platform_id やデバイスIDの型 cl_device_id はこれ自体がポインタのようなので ※、これらに該当する型は size_t としました。

※ そのため、プラットフォームID や デバイスID という表現は
   適切ではないかもしれません

node-ffi ではポインタを扱うために Buffer を使います。

そのための補助関数が ref モジュールに用意されており、下記サンプルでは以下を使っています。

  • ref モジュールの alloc を使って指定した型に応じた Buffer を作成
  • 定義した型の get を使って Buffer から値を取得

get を使えば、型のサイズやエンディアンに応じた値を Buffer から取り出してくれます。 (例えば、int32 なら Buffer の readInt32LE や readInt32BE を使って値を取得する)

なお、エラーの有無は clGetPlatformIDs・clGetDeviceIDs の戻り値が 0 かどうかで判定します。(0: 成功、0以外: エラー)

get_device_id.js
'use strict';

const ffi = require('ffi');
const ref = require('ref');

// 定数の定義
const CL_DEVICE_TYPE_DEFAULT = 1;

// ポインタ用の型定義
const uintPtr = ref.refType(ref.types.uint32);
const sizeTPtr = ref.refType('size_t');

// OpenCL の関数定義
const openCl = ffi.Library('OpenCL', {
    'clGetPlatformIDs': ['int', ['uint', sizeTPtr, uintPtr]],
    'clGetDeviceIDs': ['int', ['size_t', 'ulong', 'uint', sizeTPtr, uintPtr]]
});

// エラーチェック処理
const checkError = (errCode, title = '') => {
    if (errCode != 0) {
        throw new Error(`${title} Error: ${errCode}`);
    }
};

const platformIdsPtr = ref.alloc(sizeTPtr);

// (1) プラットフォームIDを(1つ)取得
let res = openCl.clGetPlatformIDs(1, platformIdsPtr, null);

checkError(res, 'clGetPlatformIDs');

// プラットフォームID(get を使って platformIdsPtr の先頭の値を取得)
const platformId = sizeTPtr.get(platformIdsPtr);

console.log(`platformId: ${platformId}`);

const deviceIdsPtr = ref.alloc(sizeTPtr);

// (2) デバイスIDを(1つ)取得
res = openCl.clGetDeviceIDs(platformId, CL_DEVICE_TYPE_DEFAULT, 1, deviceIdsPtr, null);

checkError(res, 'clGetDeviceIDs');

// デバイスID(get を使って deviceIdsPtr の先頭の値を取得)
const deviceId = sizeTPtr.get(deviceIdsPtr);

console.log(`deviceId: ${deviceId}`);
実行結果
> node get_device_id.js

platformId: 47812336
deviceId: 4404320

2. OpenCL のプラットフォーム情報取得

次は OpenCL のプラットフォーム情報を取得してみます。 プラットフォーム情報は clGetPlatformInfo を使って取得します。

  • (1) clGetPlatformInfo でデータサイズを取得
  • (2) バッファを確保
  • (3) clGetPlatformInfo でデータを取得
platform_info.js
'use strict';

const ffi = require('ffi');
const ref = require('ref');

// 定数の定義
const CL_PLATFORM_PROFILE = 0x0900;
const CL_PLATFORM_VERSION = 0x0901;
const CL_PLATFORM_NAME = 0x0902;
const CL_PLATFORM_VENDOR = 0x0903;
const CL_PLATFORM_EXTENSIONS = 0x0904;
const CL_PLATFORM_HOST_TIMER_RESOLUTION = 0x0905;

const uintPtr = ref.refType(ref.types.uint32);
const sizeTPtr = ref.refType('size_t');

const openCl = ffi.Library('OpenCL', {
    'clGetPlatformIDs': ['int', ['uint', sizeTPtr, uintPtr]],
    'clGetPlatformInfo': ['int', ['size_t', 'uint', 'size_t', 'pointer', sizeTPtr]]
});

const checkError = (errCode, title = '') => {
    if (errCode != 0) {
        throw new Error(`${title} Error: ${errCode}`);
    }
};

// プラットフォーム情報の出力
const printPlatformInfo = (pid, paramName) => {
    const sPtr = ref.alloc(sizeTPtr);

    // (1) データサイズを取得
    let res = openCl.clGetPlatformInfo(pid, paramName, 0, null, sPtr);

    checkError(res, 'clGetPlatformInfo size');

    // データサイズの値を取り出す
    const size = sizeTPtr.get(sPtr);

    // (2) バッファを確保
    const buf = Buffer.alloc(size);

    // (3) データを取得
    res = openCl.clGetPlatformInfo(pid, paramName, size, buf, null);

    checkError(res, 'clGetPlatformInfo data');

    // 出力
    console.log(buf.toString());
};

const platformIdsPtr = ref.alloc(sizeTPtr);

const res = openCl.clGetPlatformIDs(1, platformIdsPtr, null);

checkError(res, 'clGetPlatformIDs');

const platformId = sizeTPtr.get(platformIdsPtr);

[
    CL_PLATFORM_PROFILE,
    CL_PLATFORM_VERSION,
    CL_PLATFORM_NAME
].forEach( p => 
    printPlatformInfo(platformId, p)
);
実行結果
> node platform_info.js

FULL_PROFILE 
OpenCL 1.2  
Intel(R) OpenCL 

Keras で iris を分類

Python DeepLearning

Theano・TensorFlow 用のディープラーニングライブラリ Keras を使って、階層型ニューラルネットによる iris の分類を試してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20160531/

準備

今回は Docker コンテナで Keras を実行するため、Docker の公式イメージ python をベースに Keras をインストールした Docker イメージを用意します。

Docker イメージ作成

/vagrant/work/keras/Dockerfile
FROM python

RUN apt-get update && apt-get upgrade -y

RUN pip install --upgrade pip

RUN pip install keras
RUN pip install scikit-learn

RUN apt-get clean

sklearn の iris データセットを使うために scikit-learn もインストールしていますが、Keras を使うだけなら不要です。

また、Theano はデフォルトでインストールされるようですが、TensorFlow を使う場合は別途インストールする必要がありそうです。(今回は Theano を使います)

Dockerfile に対して docker build を実行して Docker イメージを作成します。

Docker ビルド
$ cd /vagrant/work/keras
$ docker build --no-cache -t sample/py-keras:0.1 .

(1) 学習

まずは、iris のデータセットを全て使って学習してみます。

ConvnetJS で iris を分類」 で実施したように、出力層の活性化関数はソフトマックス、損失関数(誤差関数)に交差エントロピーを使います。

sklearn の iris データセットのように、ラベルデータ (target の値) が数値 (0 ~ 2) の場合 ※ に交差エントロピーを実施するには compile の引数で loss = 'sparse_categorical_crossentropy' と指定すれば良さそうです。

※ iris.target の内容

    [0 0 0 0 0 0 0 ・・・
     0 0 0 0 0 0 0 ・・・
     1 1 1 1 1 1 1 ・・・
     2 2 2 2 2 2 2 ・・・
     2 2]

とりあえず、入力層 - 隠れ層 - 出力層 (隠れ層のニューロン数 8)というレイヤー構成を使い、学習処理(fit)はミニバッチサイズを 1 として 50 回繰り返すように指定しました。

/vagrant/work/iris_sample1.py
from keras.models import Sequential
from keras.layers.core import Dense, Activation
from sklearn import datasets

# モデルの定義
model = Sequential()

# 隠れ層の定義
model.add(Dense(input_dim = 4, output_dim = 8))
# 隠れ層の活性化関数
model.add(Activation('relu'))

# 出力層の定義
model.add(Dense(output_dim = 3))
# 出力層の活性化関数
model.add(Activation('softmax'))

model.compile(loss = 'sparse_categorical_crossentropy', optimizer = 'sgd', metrics = ['accuracy'])

iris = datasets.load_iris()

# 学習
model.fit(iris.data, iris.target, nb_epoch = 50, batch_size = 1)

docker run で Keras 用の Docker コンテナを起動した後、コンテナ内で上記を実行します。

実行例
$ docker run --rm -it -v /vagrant/work:/work sample/py-keras:0.1 bash

# cd /work
# python iris_sample1.py

Using Theano backend.
Epoch 1/50
  1/150 [..............................] - ETA: 0s - loss: 3.4213 - acc: 0.0000e  2/150 [..............................] - ETA: 0s - loss: 2.2539 - acc: 0.0000e・・・
Epoch 49/50
150/150 [==============================] - 0s - loss: 0.1225 - acc: 0.9533
Epoch 50/50
150/150 [==============================] - 0s - loss: 0.1525 - acc: 0.9333

誤差(loss)と正解率(acc)が出力されました。

(2) 学習と評価

次は、iris データセットを学習用と評価用に分割して学習と評価をそれぞれ実行してみます。

データセットを直接シャッフルする代わりに、0 ~ 149 の数値をランダムに配置した配列を numpy の random.permutation で作成し、学習・評価用のデータ分割に利用しました。

/vagrant/work/iris_sample2.py
import sys

from keras.models import Sequential
from keras.layers.core import Dense, Activation
from sklearn import datasets
import numpy as np

# 学習・評価用のデータ分割率
trainEvalRate = 0.7

# 学習の繰り返し回数
epoch = int(sys.argv[1])

# 隠れ層のニューロン数
neuNum = int(sys.argv[2])
# 隠れ層の活性化関数
act = sys.argv[3]

optm = sys.argv[4]

model = Sequential()

model.add(Dense(input_dim = 4, output_dim = neuNum))
model.add(Activation(act))

model.add(Dense(output_dim = 3))
model.add(Activation('softmax'))

model.compile(
    loss = 'sparse_categorical_crossentropy', 
    optimizer = optm, 
    metrics = ['accuracy']
)

iris = datasets.load_iris()

data_size = len(iris.data)
train_size = int(data_size * trainEvalRate)

perm = np.random.permutation(data_size)

# 学習用データ
x_train = iris.data[ perm[0:train_size] ]
y_train = iris.target[ perm[0:train_size] ]

# 学習
model.fit(x_train, y_train, nb_epoch = epoch, batch_size = 1)

print('-----')

# 評価用データ
x_test = iris.data[ perm[train_size:] ]
y_test = iris.target[ perm[train_size:] ]

# 評価
res = model.evaluate(x_test, y_test, batch_size = 1)

print(res)
実行例
# python iris_sample2.py 50 6 sigmoid adam

Using Theano backend.
Epoch 1/50
105/105 [==============================] - 0s - loss: 1.0751 - acc: 0.3524
Epoch 2/50
105/105 [==============================] - 0s - loss: 1.0417 - acc: 0.3524
・・・
Epoch 49/50
105/105 [==============================] - 0s - loss: 0.3503 - acc: 0.9714
Epoch 50/50
105/105 [==============================] - 0s - loss: 0.3458 - acc: 0.9714
-----
45/45 [==============================] - 0s
[0.35189295262098313, 0.97777777777777775]

JMX で Java Flight Recorder (JFR) を実行する

Java Groovy JMX

Java Flight Recorder (JFR) は Java Mission Control (jmc) や jcmd コマンドから実行できますが、今回は以下の MBean を使って JMX から実行してみます。

  • com.sun.management:type=DiagnosticCommand

この MBean は以下のような操作を備えており(戻り値は全て String)、jcmd コマンドと同じ事ができるようです。

  • jfrCheck
  • jfrDump
  • jfrStop
  • jfrStart
  • vmCheckCommercialFeatures
  • vmCommandLine
  • vmFlags
  • vmSystemProperties
  • vmUnlockCommercialFeatures
  • vmUptime
  • vmVersion
  • vmNativeMemory
  • gcRotateLog
  • gcRun
  • gcRunFinalization
  • gcClassHistogram
  • gcClassStats
  • threadPrint

(a) JFR の実行

JMX を使う方法はいくつかありますが、今回は Attach API でローカルの VM へアタッチし、startLocalManagementAgent メソッドJMX エージェントを適用する方法を用いました。

DiagnosticCommand には java.lang.management.ThreadMXBean のようなラッパーが用意されていないようなので GroovyMBean を使う事にします。

jfrStart の引数は jcmd コマンドと同じものを String 配列にして渡すだけのようです。(jfrStart 以外も基本的に同じ)

また、JFR の実行には Commercial Features のアンロックが必要です。

jfr_run.groovy
import com.sun.tools.attach.VirtualMachine

import javax.management.remote.JMXConnectorFactory
import javax.management.remote.JMXServiceURL

def pid = args[0]
def duration = args[1]
def fileName = args[2]

// 指定の JVM プロセスへアタッチ
def vm = VirtualMachine.attach(pid)

try {
    // JMX エージェントを適用
    def jmxuri = vm.startLocalManagementAgent()

    JMXConnectorFactory.connect(new JMXServiceURL(jmxuri)).withCloseable {
        def server = it.getMBeanServerConnection()

        // MBean の取得
        def bean = new GroovyMBean(server, 'com.sun.management:type=DiagnosticCommand')

        // Commercial Features のアンロック (JFR の実行に必要)
        println bean.vmUnlockCommercialFeatures()

        // JFR の開始
        println bean.jfrStart([
            "duration=${duration}",
            "filename=${fileName}",
            'delay=10s'
        ] as String[])
    }
} finally {
    vm.detach()
}

実行例

apache-tomcat-9.0.0.M4 へ適用してみます。

Tomcat 実行
> startup

以下の環境で実行しました。

  • Groovy 2.4.6
  • Java SE 8u92 64bit版
JFR 実行
> jps

4576 Jps
2924 Bootstrap

> groovy jfr_run.groovy 2924 1m sample1.jfr

Commercial Features now unlocked.

Recording 1 scheduled to start in 10 s. The result will be written to:

C:\・・・\apache-tomcat-9.0.0.M4\apache-tomcat-9.0.0.M4\bin\sample1.jfr

jfrStart は JFR の完了を待たずに戻り値を返すため、JFR の実行状況は別途確認する事になります。

出力結果 Recording 1 scheduled1 が recoding の番号で、この番号を使って JFR の状態を確認できます。

ファイル名を相対パスで指定すると対象プロセスのカレントディレクトリへ出力されるようです。 (今回は Tomcat の bin ディレクトリへ出力されました)

(b) JFR の状態確認

JFR の実行状況を確認するには jfrCheck を使います。

下記では recording の番号を指定し、該当する JFR の実行状況を出力しています。

jfrCheck の引数が null の場合は全ての JFR 実行状態を取得するようです。

jfr_check.groovy
import com.sun.tools.attach.VirtualMachine

import javax.management.remote.JMXConnectorFactory
import javax.management.remote.JMXServiceURL

def pid = args[0]
String[] params = (args.length > 1)? ["recording=${args[1]}"]: null

def vm = VirtualMachine.attach(pid)

try {
    def jmxuri = vm.startLocalManagementAgent()

    JMXConnectorFactory.connect(new JMXServiceURL(jmxuri)).withCloseable {
        def server = it.getMBeanServerConnection()

        def bean = new GroovyMBean(server, 'com.sun.management:type=DiagnosticCommand')

        println bean.jfrCheck(params)
    }

} finally {
    vm.detach()
}

実行例

recording 番号(下記では 1)を指定して実行します。

実行例1 (JFR 実行中)
> groovy jfr_check.groovy 2924 1

Recording: recording=1 name="sample1.jfr" duration=1m filename="sample1.jfr" compress=false (running)
実行例2 (JFR 完了後)
> groovy jfr_check.groovy 2924 1

Recording: recording=1 name="sample1.jfr" duration=1m filename="sample1.jfr" compress=false (stopped)

今回作成したサンプルのソースは http://github.com/fits/try_samples/tree/master/blog/20160519/

JDI でオブジェクトの世代(Young・Old)を判別する2

Java Groovy JDI

前回 の処理を sun.jvm.hotspot.oops.ObjectHeap を使って高速化してみたいと思います。(世代の判別方法などは前回と同じ)

使用した環境は前回と同じです。

ソースは http://github.com/fits/try_samples/tree/master/blog/20160506/

ObjectHeap で Oop を取得

ObjectReference の代わりに、sun.jvm.hotspot.oops.ObjectHeapiterate(HeapVisitor) メソッドを使えば Oop を取得できます。

今回のような方法では、以下の理由で iterate メソッドの引数へ SAJDIClassLoader がロードした sun.jvm.hotspot.oops.HeapVisitor インターフェースの実装オブジェクトを与える必要があります。

  • JDI の内部で管理している Serviceability Agent APIsun.jvm.hotspot.jdi.SAJDIClassLoader によってロードされている

下記サンプルでは SAJDIClassLoader がロードした HeapVisitor を入手し、asType を使って実装オブジェクトを作成しています。

また、HeapVisitor の doObj で false を返すと処理を継続し、true を返すと中止 ※ するようです。

 ※ 厳密には、
    対象としている Address 範囲の while ループを break するだけで、
    その外側の(liveRegions に対する)for ループは継続するようです
    (ObjectHeap の iterateLiveRegions メソッドのソース参照)

なお、ObjectHeap は sun.jvm.hotspot.jdi.VirtualMachineImpl から saObjectHeap() で取得するか、sun.jvm.hotspot.runtime.VM から取得します。

check_gen2.groovy
import com.sun.jdi.Bootstrap

def pid = args[0]
def prefix = (args.length > 1)? args[1]: ''

def manager = Bootstrap.virtualMachineManager()

def connector = manager.attachingConnectors().find {
    it.name() == 'sun.jvm.hotspot.jdi.SAPIDAttachingConnector'
}

def params = connector.defaultArguments()
params.get('pid').setValue(pid)

def vm = connector.attach(params)

// 世代の判定処理を返す
generation = { heap ->
    def hasYoungGen = heap.metaClass.getMetaMethod('youngGen') != null

    [
        young: hasYoungGen? heap.youngGen(): heap.getGen(0),
        old: hasYoungGen? heap.oldGen(): heap.getGen(1)
    ]
}

try {
    def uv = vm.saVM.universe

    def gen = generation(uv.heap())

    def youngGen = gen.young
    def oldGen = gen.old

    println "*** youngGen=${youngGen}, oldGen=${oldGen}"
    println ''

    def objHeap = vm.saObjectHeap()
    // 以下でも可
    //def objHeap = vm.saVM.objectHeap

    // SAJDIClassLoader がロードした HeapVisitor インターフェースを取得
    def heapVisitorCls = uv.class.classLoader.loadClass('sun.jvm.hotspot.oops.HeapVisitor')

    // SAJDIClassLoader がロードした HeapVisitor インターフェースを実装
    def heapVisitor = [
        prologue: { size -> },
        epilogue: {},
        doObj: { oop ->
            def clsName = oop.klass.name.asString()

            if (clsName.startsWith(prefix)) {
                def age = oop.mark.age()

                // 世代の判別
                def inYoung = youngGen.isIn(oop.handle)
                def inOld = oldGen.isIn(oop.handle)

                def identityHash = ''

                try {
                    identityHash = Long.toHexString(oop.identityHash())
                } catch (e) {
                }

                println "class=${clsName}, hash=${identityHash}, handle=${oop.handle}, age=${age}, inYoung=${inYoung}, inOld=${inOld}"
            }

            // 処理を継続する場合は false を返す
            false
        }
    ].asType(heapVisitorCls)

    objHeap.iterate(heapVisitor)

} finally {
    vm.dispose()
}

動作確認

前回と同じように、実行中の apache-tomcat-9.0.0.M4 へ適用してみました。

前回と異なり、クラス名が '/' で区切られている点に注意

実行例1 (Windows の場合)
> jps

3604 Bootstrap
4516 Jps
> groovy -cp %JAVA_HOME%/lib/sa-jdi.jar check_gen2.groovy 3604 org/apache/catalina/core/StandardContext

*** youngGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSYoungGen@0x0000000002149ab0, oldGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSOldGen@0x0000000002149b40

class=org/apache/catalina/core/StandardContextValve, hash=0, handle=0x00000000c3a577d0, age=1, inYoung=false, inOld=true
class=org/apache/catalina/core/StandardContext$NoPluggabilityServletContext, hash=0, handle=0x00000000c3a633d8, age=0, inYoung=false, inOld=true
class=org/apache/catalina/core/StandardContext$ContextFilterMaps, hash=0, handle=0x00000000c3a63ef0, age=1, inYoung=false, inOld=true
class=org/apache/catalina/core/StandardContext$NoPluggabilityServletContext, hash=0, handle=0x00000000ebc46da0, age=0, inYoung=true, inOld=false
class=org/apache/catalina/core/StandardContext, hash=6f2d2815, handle=0x00000000eddfeaa0, age=1, inYoung=true, inOld=false
class=org/apache/catalina/core/StandardContext, hash=21f2e66b, handle=0x00000000eddff238, age=3, inYoung=true, inOld=false
・・・
実行例2 (Linux の場合)
$ jps

2778 Jps
2766 Bootstrap
$ groovy -cp $JAVA_HOME/lib/sa-jdi.jar check_gen2.groovy 2766 org/apache/catalina/core/StandardContext

*** youngGen=sun.jvm.hotspot.memory.DefNewGeneration@0x00007f0760019cb0, oldGen=sun.jvm.hotspot.memory.TenuredGeneration@0x00007f076001bfc0

class=org/apache/catalina/core/StandardContext, hash=497fe2c4, handle=0x00000000f821bf90, age=0, inYoung=true, inOld=false
class=org/apache/catalina/core/StandardContext$ContextFilterMaps, hash=0, handle=0x00000000f821c5d8, age=0, inYoung=true, inOld=false
class=org/apache/catalina/core/StandardContextValve, hash=0, handle=0x00000000f821ca60, age=0, inYoung=true, inOld=false
・・・
class=org/apache/catalina/core/StandardContext, hash=5478de1a, handle=0x00000000fb12b310, age=1, inYoung=false, inOld=true
class=org/apache/catalina/core/StandardContext$NoPluggabilityServletContext, hash=0, handle=0x00000000fb12f6b0, age=0, inYoung=false, inOld=true
class=org/apache/catalina/core/StandardContext$ContextFilterMaps, hash=0, handle=0x00000000fb131a80, age=0, inYoung=false, inOld=true
class=org/apache/catalina/core/StandardContextValve, hash=0, handle=0x00000000fb1398b0, age=0, inYoung=false, inOld=true

JDI でオブジェクトの世代(Young・Old)を判別する

Java Groovy JDI

前回、オブジェクトの age を取得しましたが、同様の方法で今回はオブジェクトが Young 世代(New 領域)と Old 世代(Old 領域) のどちらに割り当てられているかを判別してみたいと思います。 (ただし、結果の正否は確認できていません)

使用した環境は前回と同じです。

ソースは http://github.com/fits/try_samples/tree/master/blog/20160430/

Young・Old 世代の判別

さて、Young・Old の判別方法ですが。

Serviceability Agent API を見てみると sun.jvm.hotspot.gc_implementation.parallelScavenge パッケージに PSYoungGenPSOldGen というクラスがあり、isIn(Address) メソッドで判定できそうです。

更に PSYoungGen と PSOldGen は sun.jvm.hotspot.gc_implementation.parallelScavenge.ParallelScavengeHeap から取得できます。

Address (sun.jvm.hotspot.debugger パッケージ所属) は sun.jvm.hotspot.oops.OopgetHandle()getMark().getAddress() で取得できるので (下記サンプルでは getHandle を使用)、ParallelScavengeHeap を取得すれば何とかなりそうです。

実際に試してみたところ、ParallelScavengeHeap を取得できたのは Windows 環境で、Linux 環境では GenCollectedHeap を使った別の方法 (getGen メソッドを使う) が必要でした。 (GC の設定等によって更に変わるかもしれません)

世代の判定クラス
実行環境 ヒープクラス ※ Young 世代の判定クラス Old 世代の判定クラス
Windows ParallelScavengeHeap PSYoungGen PSOldGen
Linux GenCollectedHeap DefNewGeneration TenuredGeneration
 ※ Universe の heap() メソッド戻り値の実際の型
    CollectedHeap のサブクラス

上記を踏まえて、前回の処理をベースに以下を追加してみました。

  • (1) sun.jvm.hotspot.jdi.VirtualMachineImpl から sun.jvm.hotspot.runtime.VM オブジェクトを取り出す ※1
  • (2) VM オブジェクトから sun.jvm.hotspot.memory.Universe オブジェクトを取得
  • (3) Universe オブジェクトから CollectedHeap (のサブクラス) を取得 ※2
  • (4) (3) の結果から世代を判定するオブジェクトをそれぞれ取得

(4) で妥当な条件分岐の仕方が分からなかったので、とりあえず youngGen メソッドが無ければ GenCollectedHeap として処理するようにしました。

 ※1 private フィールドの saVM か、package メソッドの saVM() で取得

 ※2 今回のやり方では、Windows は ParallelScavengeHeap、
     Linux は GenCollectedHeap でした

JDI の SAPIDAttachingConnector で attach した結果が VirtualMachineImpl となります。

また、SAPIDAttachingConnector でデバッグ接続した場合 (読み取り専用のデバッグ接続)、デバッグ対象オブジェクトのメソッド (hashCode や toString 等) を呼び出せないようなので、オブジェクトを識別するための情報を得るため identityHash を使ってみました。 (ただし、戻り値が 0 になるものが多数ありました)

check_gen.groovy
import com.sun.jdi.Bootstrap

def pid = args[0]
def prefix = args[1]

def manager = Bootstrap.virtualMachineManager()

def connector = manager.attachingConnectors().find {
    it.name() == 'sun.jvm.hotspot.jdi.SAPIDAttachingConnector'
}

def params = connector.defaultArguments()
params.get('pid').setValue(pid)

def vm = connector.attach(params)

// (4) 世代を判定するためのオブジェクトを取得
generation = { heap ->
    def hasYoungGen = heap.metaClass.getMetaMethod('youngGen') != null

    [
        // Young 世代の判定オブジェクト(PSYoungGen or DefNewGeneration)
        young: hasYoungGen? heap.youngGen(): heap.getGen(0),
        // Old 世代の判定オブジェクト(PSOldGen or TenuredGeneration)
        old: hasYoungGen? heap.oldGen(): heap.getGen(1)
    ]
}

try {
    if (vm.canGetInstanceInfo()) {

        // (1) (2)
        def uv = vm.saVM.universe

        // (3)
        def gen = generation(uv.heap())

        def youngGen = gen.young
        def oldGen = gen.old

        println "*** youngGen=${youngGen}, oldGen=${oldGen}"
        println ''

        vm.allClasses().findAll { it.name().startsWith(prefix) }.each { cls ->
            println cls.name()

            cls.instances(0).each { inst ->
                def oop = inst.ref()
                def age = oop.mark.age()

                // 世代の判別
                def inYoung = youngGen.isIn(oop.handle)
                def inOld = oldGen.isIn(oop.handle)

                def identityHash = ''

                try {
                    identityHash = Long.toHexString(oop.identityHash())
                } catch (e) {
                }

                println "  hash=${identityHash}, handle=${oop.handle}, age=${age}, inYoung=${inYoung}, inOld=${inOld}"
            }
        }
    }
} finally {
    vm.dispose()
}

動作確認

前回と同じように、実行中の apache-tomcat-9.0.0.M4 へ適用してみました。

実行例1 (Windows の場合)
> jps

2836 Bootstrap
5944 Jps
> groovy -cp %JAVA_HOME%/lib/sa-jdi.jar check_gen.groovy 2836 org.apache.catalina.core.StandardContext

*** youngGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSYoungGen@0x0000000002049ad0, oldGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSOldGen@0x0000000002049b60

org.apache.catalina.core.StandardContext
  hash=66dfd722, handle=0x00000000c394a990, age=0, inYoung=false, inOld=true
  hash=39504d4e, handle=0x00000000edea7cf8, age=3, inYoung=true, inOld=false
  hash=194311fa, handle=0x00000000edea8e90, age=1, inYoung=true, inOld=false
  hash=2b28e016, handle=0x00000000edf0c130, age=2, inYoung=true, inOld=false
  hash=578787b8, handle=0x00000000edf457c0, age=1, inYoung=true, inOld=false
org.apache.catalina.core.StandardContext$ContextFilterMaps
  hash=0, handle=0x00000000c394e7d0, age=0, inYoung=false, inOld=true
  hash=0, handle=0x00000000c396ec90, age=2, inYoung=false, inOld=true
  hash=0, handle=0x00000000c3988eb0, age=1, inYoung=false, inOld=true
  hash=0, handle=0x00000000edf04320, age=1, inYoung=true, inOld=false
  hash=0, handle=0x00000000edf70988, age=1, inYoung=true, inOld=false
・・・
> groovy -cp %JAVA_HOME%/lib/sa-jdi.jar check_gen.groovy 2836 org.apache.catalina.LifecycleEvent

*** youngGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSYoungGen@0x0000000002049ad0, oldGen=sun.jvm.hotspot.gc_implementation.parallelScavenge.PSOldGen@0x0000000002049b60

org.apache.catalina.LifecycleEvent
  hash=0, handle=0x00000000c37459c0, age=0, inYoung=false, inOld=true
  hash=0, handle=0x00000000c374ed40, age=1, inYoung=false, inOld=true
  hash=0, handle=0x00000000c39ff950, age=0, inYoung=false, inOld=true
  hash=0, handle=0x00000000ebb8ef90, age=0, inYoung=true, inOld=false
  hash=0, handle=0x00000000ebb90490, age=0, inYoung=true, inOld=false
  hash=0, handle=0x00000000ebb904c0, age=0, inYoung=true, inOld=false
  ・・・
実行例2 (Linux の場合)
$ jps

2801 Jps
2790 Bootstrap
$ groovy -cp $JAVA_HOME/lib/sa-jdi.jar check_gen.groovy 2790 org.apache.catalina.core.StandardContext

*** youngGen=sun.jvm.hotspot.memory.DefNewGeneration@0x00007fca50019cb0, oldGen=sun.jvm.hotspot.memory.TenuredGeneration@0x00007fca5001bfc0

org.apache.catalina.core.StandardContext
  hash=27055bff, handle=0x00000000fb025d38, age=1, inYoung=false, inOld=true
  hash=5638a30f, handle=0x00000000fb1270a8, age=1, inYoung=false, inOld=true
  hash=15fad243, handle=0x00000000fb296730, age=1, inYoung=false, inOld=true
  hash=36c4d4a0, handle=0x00000000fb2f3cf0, age=1, inYoung=false, inOld=true
  hash=33309557, handle=0x00000000fb2f3ef8, age=1, inYoung=false, inOld=true
org.apache.catalina.core.StandardContextValve
  hash=0, handle=0x00000000fb045ad8, age=0, inYoung=false, inOld=true
  hash=0, handle=0x00000000fb135300, age=0, inYoung=false, inOld=true
  hash=0, handle=0x00000000fb2ad568, age=1, inYoung=false, inOld=true
  hash=0, handle=0x00000000fb3022b0, age=1, inYoung=false, inOld=true
  hash=0, handle=0x00000000fb3050e8, age=1, inYoung=false, inOld=true
・・・
$ groovy -cp $JAVA_HOME/lib/sa-jdi.jar check_gen.groovy 2790 org.apache.catalina.LifecycleEvent

*** youngGen=sun.jvm.hotspot.memory.DefNewGeneration@0x00007fca50019cb0, oldGen=sun.jvm.hotspot.memory.TenuredGeneration@0x00007fca5001bfc0

org.apache.catalina.LifecycleEvent
  hash=0, handle=0x00000000f82079a8, age=0, inYoung=true, inOld=false
  hash=0, handle=0x00000000f8207a00, age=0, inYoung=true, inOld=false
  hash=0, handle=0x00000000f8210470, age=0, inYoung=true, inOld=false
  ・・・
  hash=0, handle=0x00000000f8506568, age=0, inYoung=true, inOld=false
  hash=0, handle=0x00000000fb003370, age=0, inYoung=false, inOld=true

この結果の正否はともかく、一応は判別できているように見えます。

ちなみに、前回と同様に処理が遅い(重い)点に関しては、Oop を Serviceability Agent APIsun.jvm.hotspot.oops.ObjectHeap で取得するように変更すれば改善できます。

注意点

今回のように JDI の内部で管理している Serviceability Agent API を取り出して使う場合の注意点は以下の通りです。

  • JDI 内部の Serviceability Agent API のクラス(インターフェースも含む)は sun.jvm.hotspot.jdi.SAJDIClassLoader クラスローダーによってロードされる

同じ名称のクラスでもロードするクラスローダーが異なれば別物となりますので、Java で今回のような処理を実装しようとすると、クラスのキャストができずリフレクション等を多用する事になると思います。

また、Groovy でも HeapVisitor 等を使う場合に多少の工夫が必要になります。