kivantium活動日記

プログラムを使っていろいろやります

pybind11とNumCppによるPythonとC++の連携

Pythonで書いたコードの速度が遅いときに一部の関数だけC++で記述したい場合があります。pybind11を使えばC++Pythonを比較的容易に連携させることができるのですが、NumPyオブジェクトをC++に渡したときの処理の記述が非常に面倒です。そこでNumPyのC++版として開発されているライブラリのNumCppを使って、NumPyオブジェクトの受け渡しを行う方法を検討しました。

TL; DR 現時点ではNumCppよりもEigenを使うほうが良さそうです

pybind11だけを使った場合

まずはpybind11だけでNumPyを使うとどうなるかについて説明します。pybind11ドキュメントのNumPyのページにある足し算を行う例を紹介します。

py::array_t<double> add_arrays(py::array_t<double> input1, py::array_t<double> input2) {
    py::buffer_info buf1 = input1.request(), buf2 = input2.request();

    /* shapeのチェックは省略 */

    auto result = py::array_t<double>(buf1.size);

    py::buffer_info buf3 = result.request();

    double *ptr1 = static_cast<double *>(buf1.ptr);
    double *ptr2 = static_cast<double *>(buf2.ptr);
    double *ptr3 = static_cast<double *>(buf3.ptr);

    for (size_t idx = 0; idx < buf1.shape[0]; idx++)
        ptr3[idx] = ptr1[idx] + ptr2[idx];

    return result;
}

行列の要素にアクセスするためにわざわざポインタを経由する必要がある上に、単なる行列の足し算をわざわざループで書き直す必要があります。この例は1次元配列でしたが、2次元以上になるとさらに処理が面倒になります。そこでC++の行列演算ライブラリを組み合わせて簡単に記述できるようにします。

pybind11は公式でEigenとの連携をサポートしているのですが、Eigenを使う方法は既に日本語解説が存在しているのでここではNumCppを使ってみます。

NumCppを使った場合

インストール

pybind11のインストールはpipを使う方法が一番簡単です。その他の方法はドキュメントのInstalling the libraryを参照してください。

pip install pybind11

NumCppはheader-onlyライブラリなのでファイルをダウンロードするだけです。ここでは作業ディレクトリにcloneすることにします。

git clone https://github.com/dpilger26/NumCpp.git

簡単な例

NumCppを使って二つの行列の和を求める関数を書いてみます。

以下のC++ファイルを example.cpp という名前で保存します。

#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>

#include <NumCpp.hpp>

namespace py=pybind11;

py::array_t<double> add_arrays(py::array_t<double, py::array::c_style> input1, py::array_t<double, py::array::c_style> input2) {
    auto array1 = nc::pybindInterface::pybind2nc(input1);
    auto array2 = nc::pybindInterface::pybind2nc(input2);
    auto sum = array1 + array2;
    return nc::pybindInterface::nc2pybind(sum);
}

PYBIND11_MODULE(example, m) {
    m.def("add_arrays", &add_arrays);
}

Linuxの場合コンパイルは以下のように行います。

c++ -O3 -Wall -shared -std=c++14 -fPIC $(python3 -m pybind11 --includes) -I NumCpp/include/ -DNUMCPP_INCLUDE_PYBIND_PYTHON_INTERFACE example.cpp -o example$(python3-config --extension-suffix)

これはpybind11のコンパイルコマンドに NumCppのヘッダファイルの場所指定 (-I NumCpp/include/) と NumCppのpybind11オプション (-DNUMCPP_INCLUDE_PYBIND_PYTHON_INTERFACE) を追加してC++14を指定 (-std=c++14) したものです。CMakeを使う方法などもありますが、複雑なのでここでは解説しません。

コンパイルに成功すると example.cpython-***.so のようなファイルが生成されます。これを読み込むPythonプログラムは以下のようになります。

import numpy as np
import example

a = np.asarray([1, 2, 3, 4]) 
b = np.asarray([2, 3, 4, 5])
print(example.add_arrays(a, b).shape) # [[3. 5. 7. 9.]]  

正しく実行できているように見えるのですが、1次元配列同士の和なのに2次元配列が返ってきています。ドキュメントによると、NumCppのNdArrayクラスは2次元配列として実装されているため、1次元配列は1xNの2次元配列として扱う仕様になっているそうです。

2次元配列は問題なく処理できます。

import numpy as np
import example

a = np.asarray([[1, 2], [3, 4]]) 
b = np.asarray([[2, 3], [4, 5]])
print(example.add_arrays(a, b)) 
# [[3. 5.]
#  [7. 9.]]

しかし、3次元配列を渡すとエラーが起こります。

import numpy as np
import example

a = np.asarray([[[1], [2]], [[3], [4]]]) 
b = np.asarray([[[2], [3]], [[4], [5]]]) 
print(example.add_arrays(a, b)) 

エラーメッセージ

File: NumCpp/include/NumCpp/PythonInterface/PybindInterface.hpp
    Function: pybind2nc
    Line: 88
    Error: input array must be no more than 2 dimensional.Traceback (most recent call last):
  File "test.py", line 6, in <module>
    print(example.add_arrays(a, b)) 
ValueError: File: NumCpp/include/NumCpp/PythonInterface/PybindInterface.hpp
    Function: pybind2nc
    Line: 88
    Error: input array must be no more than 2 dimensional.

多次元配列についてのissueを見る限り3次元以上の配列はまだ実装されてなさそうなので、現時点でNumPy互換と呼ぶのはちょっと難しいかもしれません。

おまけ: Eigenを使った場合

参考としてEigenを使った場合を示しておきます。MatrixをRowMajorにすることと、VectorとMatrixを区別することだけ気をつければそんなに難しくないので、よりメジャーなEigenを使うほうが良い場合が多いと思います。

#include <pybind11/pybind11.h>
#include <pybind11/eigen.h>

#include <Eigen/Dense>

namespace py = pybind11;

using RowMatrixXd = Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>;

template <typename T>
T add_arrays(Eigen::Ref<const T> input1, Eigen::Ref<const T> input2) {
    return input1 + input2;
}

PYBIND11_MODULE(example, m)
{
    m.def("add_vectors", &add_arrays<Eigen::VectorXd>);
    m.def("add_matrices", &add_arrays<RowMatrixXd>);
}

Pythonはこんな感じです。

import numpy as np
import example

a = np.asarray([1, 2, 3, 4]) 
b = np.asarray([2, 3, 4, 5])
print(example.add_vectors(a, b))  # array([3., 5., 7., 9.])

a = np.asarray([[1, 2], [3, 4]])
b = np.asarray([[2, 3], [4, 5]])
print(example.add_matrices(a, b))
# array([[3., 5.],
#        [7., 9.]])

参考文献

1つ目の記事のコメントによるとxtensorというライブラリがあり、これもpybind11連携ができるようなのですが、インストールにcondaが必要そうなのでまだ試していません。誰か試していただけると嬉しいです。

追記 xtensorを試しました。condaは必要ありませんでした。 kivantium.hateblo.jp

創作+機械学習 Advent Calendar 2021 を開催します

このAdvent Calendarは創作(小説、漫画、アニメ、イラスト、映画、音楽、ゲーム等)と機械学習に関連した記事を投稿してもらい、優れた記事を書いた方に賞を贈呈する企画です。

2019年に@thetenthartさん主催の創作+機械学習LT会というイベントがあったのですが、コロナの影響で第2回が開催できていませんでした。そこで@thetenthartさんと@xbar_usuiさんの協力を得て、Advent Calendarという形で後継のイベントを企画させていただきました。皆様のご参加をお待ちしております。

adventar.org

f:id:kivantium:20211104124330p:plain:w600
告知画像(@yumu_7さんに作っていただきました)

ルール

  • 参加の意思表示はAdventarに記事公開日を登録することで行います。
  • 登録日になったら創作(小説、漫画、アニメ、イラスト、映画、音楽、ゲーム等)と機械学習に関連する記事を公開してください(追記: 「記事」はインターネット上で自由に閲覧できるものであれば形式は問いません。ブログ・動画・漫画などを想定しています。機械学習で生成した創作物のみでも良いですが、機械学習がどう使われているかが分かる解説があることが望ましいです。)
  • 2021年内に記事を公開された方を賞の審査対象とします(追記: 空き枠に後から登録することも可能です)
  • 2022年1月に参加者の相互投票で優秀賞を決定します。
  • 賞に関する連絡を送る場合がありますので、Twitter@kivantiumからのDMを受信できる状態にするか、メールアドレスなどの連絡先を記事中に記載してください。
  • 記事を投稿してくださった方にはコミケや技術書典等で出版する同人誌への投稿を後日依頼する可能性があります。

記事の書き方

  • 記事の冒頭に創作+機械学習 Advent Calendar 2021 の記事であることを明記してください。
  • 記事の内容は創作と機械学習に関連していればなんでもありです。未完成のものや企画案などでも大丈夫です。
  • 機械学習の定義はなるべく広く取ります。あなたが機械学習だと思うものが機械学習です。
  • 記事は日本語または英語で書いてください。
  • 一人で複数の記事を投稿しても問題ありません。カレンダーの枠が埋まった場合は2枚目を作成します。

テーマの例

  • 画像関係: 画像分類・画像生成・自動着色・超解像など
  • 言語関係: 小説生成・チャットボットなど
  • 音声関係: 音楽生成・声質変換など
  • 創作支援: 機械学習による創作支援ツールの紹介・ツールに支援されて作った作品の紹介など
  • その他: ゲームの自動プレイ・推薦システムの構築など

上に挙げた以外のテーマでももちろん大丈夫です。未完成のものやアイデア段階の記事も歓迎します。「〜というデータを集めて、自動で〜する機械学習モデルを作りたいと思っています。協力者募集」みたいな感じでも問題ありません。2019年のLT会のスライドも参考になると思います。

参考までに本ブログで関係しそうな過去記事を紹介しておきます。

賞について

以下の賞を用意しています。審査員特別賞を提供してくださる方がいらっしゃったら連絡してください。

  • 最優秀賞 (1名) 賞金20000円
  • 優秀賞(2〜3名)賞金10000円
  • 審査員特別賞
    • @kivantium賞(機械学習の使い方に新しさを感じる記事を書いた方に授与)
    • @thetenthart賞(創作支援に役立ちそうな記事を書いた方に授与)
    • @xbar_usui賞(機械学習を知らない人でも楽しめる記事を書いた方に授与)

賞金はAmazonギフトカードでの支払いを予定していますが、希望があれば銀行振込やビットコイン支払いなどにも柔軟に対応します。 皆様の投稿をお待ちしております。

【宣伝】創作と機械学習に関するDiscordサーバーがあるので、興味がある方はご参加ください。本Advent Calendarの運営についてもこのDiscordで議論しています

招待リンク: https://discord.gg/jQNXjkrqGU

FlaskでWebSocketを使う

WebSocketを使うとリアルタイム性の高いWebアプリケーションを作ることができます。今回はFlaskでWebSocketを使うライブラリのFlask-SocketIOを使って、複数のブラウザでテキストボックスを同期するアプリを作成します。

インストール

Flask-SocketIOはpipでインストールできます。

pip install flask-socketio

サンプル

Google Documentの同時編集みたいなイメージで、以下の機能を持つアプリを作成します。

  • 接続中のユーザー数が表示される
  • 接続中のユーザー全員でテキストボックスに入力した内容が同期される

DOMの操作はjQueryで行うことにします。作成したソースコードは以下の通りです。

server.py

from flask import Flask, render_template, request
from flask_socketio import SocketIO, send, emit

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'

# cors_allowed_originは本来適切に設定するべき
socketio = SocketIO(app, cors_allowed_origins='*')

# ユーザー数
user_count = 0
# 現在のテキスト
text = ""

@app.route('/')
def index():
    return render_template('index.html')

# ユーザーが新しく接続すると実行
@socketio.on('connect')
def connect(auth):
    global user_count, text
    user_count += 1
    # 接続者数の更新(全員向け)
    emit('count_update', {'user_count': user_count}, broadcast=True)
    # テキストエリアの更新
    emit('text_update', {'text': text})


# ユーザーの接続が切断すると実行
@socketio.on('disconnect')
def disconnect():
    global user_count
    user_count -= 1
    # 接続者数の更新(全員向け)
    emit('count_update', {'user_count': user_count}, broadcast=True)


# テキストエリアが変更されたときに実行
@socketio.on('text_update_request')
def text_update_request(json):
    global text
    text = json["text"]
    # 変更をリクエストした人以外に向けて送信する
    # 全員向けに送信すると入力の途中でテキストエリアが変更されて日本語入力がうまくできない
    emit('text_update', {'text': text}, broadcast=True, include_self=False)


if __name__ == '__main__':
    # 本番環境ではeventletやgeventを使うらしいが簡単のためデフォルトの開発用サーバーを使う
    socketio.run(app, debug=True)

templates/index.html

<html>
  <head>
    <title>FlaskでWebSocket</title>
  </head>
  <body>
    <p>現在の接続者数<span id="user_count"></span></p>
    <textarea id="text" name="text" rows="10" cols="60"></textarea>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js" integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA==" crossorigin="anonymous"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>

    <script type="text/javascript" charset="utf-8">
      var socket = io();

      // 接続者数の更新
      socket.on('count_update', function(msg) {
        $('#user_count').html(msg.user_count);
      });
      
      // テキストエリアの更新
      socket.on('text_update', function(msg) {
        $('#text').val(msg.text);
      });
      
      // テキストエリアが変更されると呼び出される
      $('#text').on('change keyup input', function() {
        socket.emit('text_update_request', {text: $(this).val()});
      });
    </script>
  </body>
</html>

グローバル変数で接続者数をカウントするのはスレッドセーフではないのですが、GitHubでの作者コメントを読む限りでは通常このコードで問題ないようです。

python server.pyで実行して、ブラウザの画面を複数開くと同期している様子を見ることができます。 入力や削除の様子まで同期されていることが分かると思います。

f:id:kivantium:20211018102437g:plain

以上です。

Twitterのハッシュタグを正しく抽出する

ここ一年くらいかけて暇な時間にTwitterに投稿されたイラストを収集するにじさーちというサイトを開発しています。
にじさーちにはハッシュタグを使って画像を検索する機能があるのですが、ハッシュタグに中黒(・)が含まれる場合にうまく抽出できないバグがありました。

例えば次のツイートには「リネット・ビショップ生誕祭2020」というハッシュタグが含まれています。しかし、今まで使っていた正規表現では中黒でハッシュタグが終わると判定されてしまい「リネット」というタグがついていると認識されていました。

ハッシュタグ正規表現で抽出するのはかなり難しい

Twitterでは#で始まる文字列がハッシュタグとして利用されていますが、多言語対応を考えるとハッシュタグがどこで終わるのかを正確に判定する正規表現を書くのはかなり難しいです。例えば、hashtag regex in python · GitHubでは

hashtag_re = re.compile("(?:^|\s)[##]{1}(\w+)", re.UNICODE)

のようにハッシュタグを抽出しています。にじさーちでは昨日までこの正規表現をコピペして使っていたのですが、中黒(・)がハッシュタグの一部として認識されていませんでした(おそらく「・」がUnicode word charactersに含まれず\wにマッチしなかったのでしょう)

また、Twitterのソースコードから生成した正規表現だと主張するStack Oveflowのコードも中黒(・)をうまく認識できませんでした。

ハッシュタグ正規表現の歴史

Twitterの中の人ではないので詳しい歴史的経緯は分かりませんが、日本語ハッシュタグ導入時のtogetterを読むと、導入当時は中黒(・)がハッシュタグ区切りとして導入されているようでした。


(現在は・がハッシュタグの一部として認識されているのでこれだけ見てもよく分かりませんが、togetter上では次の写真のように表示されているので、当時は・がハッシュタグの区切りになっていたことが推測できます)
f:id:kivantium:20210829154405p:plain

しかし、2017年12月16日のInitial commit of twitter-text 2.0 · twitter/twitter-text@34dc1dd · GitHubでは中黒に対応するU+30FBがhashtagSpecialCharsとして追加されています。そのため、2011年から2017年のどこかのタイミングで中黒の処理に変更が入ったことが推測できます。2015年のStack Overflowの回答にある正規表現が現在の仕様と合致しないのはTwitter側の仕様変更が理由である可能性が高いです。

解決策その1: 公式ライブラリtwitter-textを使う

多言語に対応するハッシュタグ正規表現を自分で実装するのは非常に手間がかかりそうです。しかし、Twitter社が公式でテキストをパースするためのライブラリを提供しているので対応言語の場合はライブラリを使うことが出来ます。
github.com

JavaScriptの場合はこのように使うことができます。

var parser = require('twitter-text')
var tweet = "#リネット・ビショップ生誕祭2020\n間に合った!!リーネちゃん誕生日おめでとう!";
var result = parser.extractHashtags(tweet);
console.log(result);  // [ 'リネット・ビショップ生誕祭2020' ]

しかし、現在ライブラリが正式に提供されているのはJava, Ruby, JavaScript, Objective-Cのみで、Pythonは含まれていません。移植すればいいのかもしれませんが、Unicode周りを正しく書ける知識は私にはありません。(Python移植であると主張するレポジトリはあるのですが、日本語ではうまく動きませんでした)

解決策その2: Twitter APIに含まれるハッシュタグ情報を使う

Pythonによる文字列処理でハッシュタグを抽出するのは難しそうなのですが、Twitter APIを使ってツイートを読み込む際にハッシュタグ情報を取得することができます。

Twitter APIGET statuses/show/:idのドキュメントを読むと、Example Responseの"entities"の中に"hashtags"という項目があります。ここにハッシュタグの情報が含まれています。

Tweepyを使う場合は次のようにハッシュタグを取得できます。

status = api.get_status(1271018594273333248)  # 冒頭のツイート
for tag in status.entities['hashtags']:
    print(tag['text'])  # リネット・ビショップ生誕祭2020

Twitter APIを使うことで、文字列処理に頼らずにTwitterが認識する通りのハッシュタグを取得することができました。

(今回例に使ったツイートは最初にバグに気がついた例として記録しておいたものです。バグに気づいてから1年以上放置していたようです……)

OpenAI Gym Roboticsの環境構築とBaselinesの実行

OpenAIが提供している強化学習環境のGymを使って、OpenAI BaselinesのHER (Hindsight Experience Replay) を実行するまでの手順について記録しておきます。環境構築はUbuntu 20.04で行いました。

MuJoCoのライセンス取得

Gymで提供されている環境のうち、3Dを使うものはMuJoCoに依存しています。今回使いたいRobotics環境は3Dなのでライセンスを取得する必要があります。

ライセンスの取得はLicenseから行います。30日間の試用期間中は無料で利用できますが、その後は正式なライセンスを取得する必要があります。学生の個人利用は無料ですが、学生でない場合は個人の非商用利用で年間$500、商用利用では年間$2000掛かります。ぼったくり

私はまだ学生なので、学生個人利用ライセンスを申請しました。申請から数日後にaccount numberがメールで送られてきました。ライセンスページで配布されているgetidなるバイナリを実行して得られるComputer idとaccount numberを登録するとmjkey.txtというファイルをダウンロードできます。これを~/.mujoco/に保存します。

Productsで配布されているmujoco200 linuxをダウンロードします。これを解凍して~/.mujoco/mujoco200/に置きます。

~/.bashrc に以下を追記して(usernameは自分の名前にする)、シェルを再起動します。

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/username/.mujoco/mujoco200/bin

必要ライブラリのインストール

OpenAI Gym Roboticsの実行に必要なライブラリ

GymのREADMEに従って以下をインストールします。

sudo apt-get install -y libglu1-mesa-dev libgl1-mesa-dev libosmesa6-dev xvfb ffmpeg curl patchelf libglfw3 libglfw3-dev cmake zlib1g zlib1g-dev swig

MuJoCoに依存する環境を実行するためにはmujoco-pyが必要になります。

pip install -U 'mujoco-py<2.1,>=2.0'

Baselinesの実行に必要なライブラリ

BaselinesのREADMEに従って

sudo apt-get update && sudo apt-get install cmake libopenmpi-dev python3-dev zlib1g-dev

を実行します。

次にTensorFlowをインストールする必要があるのですが、今のところTensorFlow 1.14が推奨されています。自分が使っているPython環境では古すぎてインストールできなかったため、Tensorflowの旧バージョンをインストールする - Qiitaに従ってpyenvでPython 3.7.2の環境を構築してそこでインストールを行いました。

pip install tensorflow==1.14.0

その他、ドキュメントに書いていない必要なライブラリをインストールするために以下を実行します。

pip install matplotlib pandas atari_py filelock mpi4py pytest

Baselinesのインストール

OpenAI Baselinesをインストールします。

git clone https://github.com/openai/baselines.git
cd baselines
pip install -e .

インストールが終わったらテストを実行します。テストが通らなかったら適宜対応してください。

pytest

OpenAI Gym Roboticsの動作確認

まずはOpenAI Gym Roboticsが正しく動くか確認します。以下のコードを実行します。

import gym

env = gym.make('FetchReach-v1')
while True:
    observation = env.reset()
    while True:
        env.render()
        action = env.action_space.sample()
        observation, reward, done, info = env.step(action)
        if done:
            print("Done!")
            break

ランダムに選んだ動作を無限に続けるコードになっています。正しくインストールされていれば、以下のような画面が表示されます。 f:id:kivantium:20210208215102p:plain:w600

Baselinesの実行

Baselinesに実装されているHER (Hindsight Experience Replay) を実行します。このアルゴリズムは報酬がスパースな環境(例えばロボットアームによる操作が成功したかしなかったかのどちらかしか分からないような環境)でうまく強化学習を行うために開発されたアルゴリズムです。現在の方策がゴールに到達できなかった場合でも、現在の方策で実行できた範囲のものをゴールと設定して報酬を与えることで報酬のスパースさを解決しているようですが、詳細は今度実装したときにでも書きます。

OpenAIによるHERのベースライン実装を実行するには、先にインストールしたbaselinesディレクトリで

python -m baselines.run --alg=her --env=FetchReach-v1 --num_timesteps=5000 --play

を実行すればよいです。ここで実行するFetchReach-v1は、ランダムに選ばれた3次元空間上の点に手を動かすだけの簡単なタスクなので、CPUのみの環境でもすぐに100%成功するようになります。学習結果はこんな感じになりました。

次回があれば、PyTorchあたりで強化学習アルゴリズムを何か実装します。

参考サイト

Thinkpad X13 Gen1 (AMD) を買った

年末セールで Thinkpad X13 Gen1 (AMD) を買いました。

(このツイートの後で自分が学生であることを思い出して学割ストアで買ったので、最終的には164,934円でした)

Ryzen人気のせいか納期が遅いという評判で、自分のところにも納品予定が3月下旬になるというメールが一度来ましたが、結局1ヶ月で届きました。

以下、セットアップでやったことを記録しておきます。

Windows 10のセットアップ

前回Windows 10をセットアップしたときに、表示に従ってMicrosoftアカウントと紐付けたらユーザー名が日本語になってしまいました。 通常は日本語ユーザー名でも問題ないと思うのですが、Adobe CCのインストール時にユーザー名に2バイト文字が含まれていることが原因の問題を踏んだことがあるので、確実に英数字のユーザー名に設定するためにローカルアカウントでセットアップすることにしました。

Windows 10 ローカルアカウントセットアップ手順 | cloud.config Tech Blogにあるように、インターネットに繋がずにアカウントを作成すればMicrosoftアカウントの使用を回避できます。 誘導に従ってうっかりインターネットに接続してしまったのですが、Shift+F10 を押してコマンドプロンプトを立ち上げて

netsh wlan delete profile name=”ネットワーク名(無線LANならSSID)”

を実行してネットワーク接続を切断してから左上の←ボタンを押して戻れば、ローカルアカウントでセットアップできました。

Ubuntu 20.04 とのデュアルブート

普段の作業はUbuntuで行っているのでWindows 10とのデュアルブート環境を構築しました。 以下の手順には不要なものが含まれているかもしれませんが、どれが不要なのかを検証するために環境を壊したくないのでとりあえず自分がうまく行った手順を記録しておきます。

Windows 10 上で行うこと

高速スタートアップの無効化

コントロールパネル → ハードウェアとサウンド → 電源オプション → 電源ボタンの動作を選択する → 電源ボタンの定義とパスワード保護の有効化 を開き、 現在利用可能でない設定を変更します をクリックしてから、高速スタートアップを有効にする(推奨) のチェックを外して「変更の保存」を押しました。

BitLocker の無効化

スタート→設定 → 更新とセキュリティ → デバイスの暗号化 を開き、「デバイスの暗号化が有効になっています」の下にある オフにする をクリックしてBitLockerをオフにしました。

BIOSの設定

起動直後にEnterキーを連打してF2を押せばBIOS設定メニューが開きます。

Secure Boot の無効化

Security > Secure Boot > Secure Boot を開いて Secure Boot をオフにしました。

Sleep State の変更

Config > Power > Sleep State を開いて Windows 10 から Linux に変更しました。こうしないとサスペンド時に問題が起こるそうです(参考: ThinkPad X13 Gen 1 (AMD) + Ubuntu 20.04 における不具合の解決 - Qiita

ブートUSBの作成

ThinkPad X13 Gen 1 (AMD) + Ubuntu 20.04 における不具合の解決 - Qiita によると、Ubuntu 20.04 でインストールされるLinux 5.4では問題が発生するらしいので Ubuntu 20.04.1 をインストールしました。

Ubuntu Desktop 日本語 Remixのダウンロード | Ubuntu Japanese Team から20.04.1の日本語Remixをダウンロードし(Ubuntu Japanese Team のみなさんありがとうございます)、手元のUbuntu 18.04でブートUSBを作成しました。

Ubuntuのインストール

ブートUSBを差し込んだ状態で起動し、Enterを連打してブート設定に入り、F12でUSBを起動メディアに設定してUSBから起動しました。 画面の指示通りにインストールするだけで無事終わりました。ディスク容量はWindowsUbuntuで均等にしました。

Ubuntuのインストール後に行った設定

ディレクトリ名を英語にする

ターミナルを起動して

LANG=C xdg-user-dirs-gtk-update

CapsLockをCtrlにする

コマンド一発でCapsLockをCtrlに変える方法を参考にしました。

gsettings set org.gnome.desktop.input-sources xkb-options "['ctrl:nocaps']"

/etc/default/keyboardXKBOPTIONS の行を XKBOPTIONS="ctrl:nocaps" に変える方法もあるようですが、こっちはうまく動きませんでした。

Windowsの時計がずれないようにする

デュアルブートにすると、Windowsの時計が9時間ずれます。Ubuntu側で

sudo timedatectl set-local-rtc true

を実行すると直ります。

不具合について

上記の手順でインストールしたところ、ThinkPad X13 Gen 1 (AMD) + Ubuntu 20.04 における不具合の解決 - Qiitaで言及されていた画面の明るさ調整ができない問題とサスペンドが出来ない問題は発生しませんでした。 インストール直後に sudo apt upgrade したら Linux 5.8.0 が入ったので、20.04 リリース直後のカーネルでの問題は解決済みのようです。(よく観察すると起動するたびに明るさがリセットされ、明るさを1段階下げるだけで明るさが最低レベルになるので若干挙動が怪しい気もしますが、実用上問題ないです)

レビュー

ベンチマーク

今まで使っていた Thinkpad X1 Carbon (2017) との比較のためにGeekbench 5ベンチマークを取りました。 結果はこんな感じでした。

f:id:kivantium:20210128133225p:plain

詳細はここから確認できます LENOVO 20UFCTO1WW vs LENOVO 20HQCTO1WW - Geekbench Browser

マルチコアでの性能が3倍以上違うのには驚きました。Ryzenすごい。

キーボード

ラップトップのキーボードは使わない人もいますが、ラップトップにキーボードを外付けして使うのが好きになれない自分としてはキーボードも重要なポイントになります。

f:id:kivantium:20210128134401j:plain:w500

キー配列はこんな感じで、右端のキーが若干狭くなっています。慣れれば問題ないかもしれませんが、少し気になるところです。 打鍵感はMacBook Proよりはマシですが、Thinkpad X1 Carbon (2017)と比べるとちょっと固くて打ちにくいと感じました。

今後使ってみて気になるところがあったら追記します。

RISC-Vクロスコンパイラで生成したバイナリを自作RISC-V上で実行する

4連休の課題としてFPGAで簡単なCPUを作っているので、その進捗を記録しておきます。

RISC-V (RV32I) の作成

とりあえず今回は確実に動くCPUを作ることを目標にしました。 パイプラインなどは実装せず、フェッチ→デコード→実行→メモリ・アクセス→書き戻しの5段階にそれぞれ1クロック使って、1命令に5クロックかける設計になっています。 命令セットにはRISC-Vの一番基本的な構成であるRV32Iを採用しましたが、簡単のため特権命令や割り込み周りは省略しました。 ハードウェアには以前使ったDigilentのBasys 3を使って、Vivadoで開発しました。 kivantium.hateblo.jp

あまり工夫したところはないので実装の詳細は説明しません。ソースコードはここに置いてあります。 github.com

RISC-Vクロスコンパイラのインストール

RISC-V向けのGCCriscv-gnu-toolchainというリポジトリで公開されています。以前はriscv-toolsというリポジトリ以下で公開されていたものが移動したらしいので、古い記事を参考にするときは気をつけてください。 github.com

READMEに従ってインストールするだけですが、configureで32bit用に設定する必要があります。ソースコードだけで10GB近くあるのでディスク容量に注意してください。

sudo apt-get install autoconf automake autotools-dev curl python3 libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev
git clone --recursive https://github.com/riscv/riscv-gnu-toolchain
./configure --prefix=/opt/riscv32 --with-arch=rv32im --with-abi=ilp32d
make linux

/opt/riscv32以下にインストールされるのでPATHを通しておきます。

export PATH=/opt/riscv32/bin:$PATH

バイナリの作成

簡単な例として、フィボナッチ数列の第10番目の項を再帰で求めるプログラムを実行することにします。test.cを以下の通り作成します。main関数を抜けないようにするために最後に無限ループを実行しています。

int fib(int n) {
  if(n <= 1) return 1;
  return fib(n-1) + fib(n-2);
}

int main() {
  fib(10);
  for(;;) {}
  return 0;
}

これを自作CPUで動くようにコンパイルします。

普通にコンパイルしてしまうと未実装の命令を使った初期化ルーチンが走ってしまうので、まずはそれを無効にします。start.Sを以下の通り作成します。

.section .text.init;
.globl _start
_start:
    call main

何もせずにmainを呼び出すアセンブリになっています。

次に、命令を0番地から実行するように指定します。link.ldを以下の通り作成します。

OUTPUT_ARCH( "riscv" )
ENTRY(_start)

SECTIONS
{
  . = 0x00000000;
  .text.init : { *(.text.init) }
  .tohost : { *(.tohost) }
  .text : { *(.text) }
  .data : { *(.data) }
  .bss : { *(.bss) }
  _end = .;
}

以下のようにしてバイナリを生成します。

riscv32-unknown-elf-gcc -march=rv32i -c -o start.o start.S
riscv32-unknown-elf-gcc -march=rv32i -c -o test.o test.c
riscv32-unknown-elf-ld test.o start.o -lc -L/opt/riscv32/riscv32-unknown-elf/lib/ -Tlink.ld -nostartfiles -static -o test.elf
riscv32-unknown-elf-objcopy -O binary test.elf test.bin
hexdump -v -e '1/4 "%08x" "\n"' test.bin > test.hex

最後に出来上がるtest.hexには、CPUが実行する命令列が16進数で書かれています。

074000ef
fe010113
00112e23
(中略)
00a00513
f7dff0ef
0000006f

ELF形式のファイルに対してobjdumpを実行すると逆アセンブルした結果を見ることができます。

$ riscv32-unknown-elf-objdump -d test.elf

test.elf:     ファイル形式 elf32-littleriscv


セクション .text.init の逆アセンブル:

00000000 <_start>:
   0:   074000ef            jal ra,74 <main>

セクション .text の逆アセンブル:

00000004 <fib>:
   4:   fe010113            addi    sp,sp,-32
   8:   00112e23            sw  ra,28(sp)
   c:   00812c23            sw  s0,24(sp)
  10:   00912a23            sw  s1,20(sp)
  14:   02010413            addi    s0,sp,32
  18:   fea42623            sw  a0,-20(s0)
  1c:   fec42703            lw  a4,-20(s0)
  20:   00100793            li  a5,1
  24:   00e7c663            blt a5,a4,30 <fib+0x2c>
  28:   00100793            li  a5,1
  2c:   0300006f            j   5c <fib+0x58>
  30:   fec42783            lw  a5,-20(s0)
  34:   fff78793            addi    a5,a5,-1
  38:   00078513            mv  a0,a5
  3c:   fc9ff0ef            jal ra,4 <fib>
  40:   00050493            mv  s1,a0
  44:   fec42783            lw  a5,-20(s0)
  48:   ffe78793            addi    a5,a5,-2
  4c:   00078513            mv  a0,a5
  50:   fb5ff0ef            jal ra,4 <fib>
  54:   00050793            mv  a5,a0
  58:   00f487b3            add a5,s1,a5
  5c:   00078513            mv  a0,a5
  60:   01c12083            lw  ra,28(sp)
  64:   01812403            lw  s0,24(sp)
  68:   01412483            lw  s1,20(sp)
  6c:   02010113            addi    sp,sp,32
  70:   00008067            ret

00000074 <main>:
  74:   ff010113            addi    sp,sp,-16
  78:   00112623            sw  ra,12(sp)
  7c:   00812423            sw  s0,8(sp)
  80:   01010413            addi    s0,sp,16
  84:   00b00513            li  a0,11
  88:   f7dff0ef            jal ra,4 <fib>
  8c:   0000006f            j   8c <main+0x18>

C言語で書いた通り、再帰でフィボナッチ数を求めた後、`8c'を無限ループするプログラムになっていることが確認できます。

生成した16進数の命令列はSystemVerilogの$readmemhを利用して命令メモリに埋め込んでいます(ソースコード)。コードを変更するたびに論理合成をやり直す必要がありますが、命令列を外部から読み込ませるのは面倒なのでこうしました。

実機での動作

関数の引数と戻り値が入るa0レジスタの値を10進数で7セグLEDに表示する回路を組みました。しばらく計算した後、最終的な関数の返り値である89が表示されます。(動作を見やすくするためにクロックを10万分周しています)

今後の課題

xv6が去年からRISC-Vに対応したそうなので、xv6が動くようなCPUが作れると良いです。 OSを動かすためには割り込みなどの特権命令やスーパーバイザーモードでの仮想アドレスを実装しないといけないみたいので道のりは険しそうですが……

参考文献

ツールチェインの使い方が特に参考になりました。

github.com 上のFPGAマガジンで実装されているRISC-Vです(本誌からリンクされてなかった……)

1命令を5クロックで実行する設計はここから持ってきました(このコードではデコーダーやALUが順序回路になっていますが、自分の実装では組み合わせ回路になっています。2クロック無駄になっていますが、簡単のためです……)

mindchasers.com ツールチェインのコンパイルオプションはここを参考にしました。

広告コーナー