Add Extend Instruction set to RISC-V GNU Toolchain
前回に引き続き、次は拡張命令セットをGCC(RISC-V GNU Toolchain)に追加していく。前回はとりあえずSpike上で量子系の命令セットを追加して演算を行うまで行った。ここで目標としては…
1. gnu toolchainを改造して量子系の命令セットを解釈できるようにする。
2. 元のソースコードでは、asm volatile を使って命令セットを記述する。
何かc/c++向けに関数を容易すれば良いのだけど、とりあえずは asm volatile を直接叩くということで。動いてしまえば関数化してしまえば問題ない。つまり、次のような流れでコンパイル~実行することになる。
test.c code int main(int argc, char* argv[]) { int ret1 = 0; // call qooh asm volatile( "qooh.k qa0,qt1,qzero,1" ); // call qmeas asm volatile( "qmeas.k %0,qt1,qzero,1" :"=r"(ret1) : ); printf("%d\n", ret1); return 0; }
# gcc compile. riscv64-unknown-elf-gcc test.c -o test -march=rv64imafdkc # LLVM compile. clang -target riscv64-unknown-linux-gnu -c test.c -emit-llvm -o test.bc llc -march=riscv64 -relocation-model=pic -filetype=asm test.bc -o test.s riscv64-unknown-elf-gcc test.s -o test -march=rv64imafdkc
assembly code main: addi sp, sp, -48 sd ra, 40(sp) sd s0, 32(sp) addi s0, sp, 48 sw zero, -20(s0) sw a0, -24(s0) sd a1, -32(s0) sw zero, -36(s0) qooh.k qa0,qt1,qzero,1 qmeas.k a0,qt1,qzero,1 sw a0, -36(s0) lw a1, -36(s0)
これでコンパイルをした物をSpikeに与えると量子系の演算を行うことが出来る。RISC-Vの拡張命令セットで追加した”k”はQuantum用の拡張として追加している。またgcc本体にも命令セットを解釈できるよう、改造を行っていくことになる。手順を追ってソースコードの要点を絞って(gnu toolchainのコードはかなり膨大で改造をする全部の説明はさすがにムリポ)、どうやってgccに追加していくか?メモしていく。
1. build RISC-V GNU Toolchain
次のような感じで丸っと持ってきて、とりあえずビルドできるようにしておく。これでビルドできれば環境的にはOK。
git clone --recursive https://github.com/riscv/riscv-gnu-toolchain
pushd riscv-gnu-toolchain
./configure --prefix=$HOME/work/riscv --enable-multilib
make -j`nproc
2. add extension instruction set
まず行うのは命令セットの拡張を追加する。命令セットは rv64imafdkc この部分で、それぞれの意味は…
rv32i/64i : 32bit/64bit Integer Instruction
m : 整数除算・乗算
a : アトミック命令
f : 単精度浮動小数点(32bit)
d : 倍精度浮動小数点(64bit)
c : 圧縮命令
k : Quantum用
といった感じで。kを追加してあげることにする。修正部分は次のとおり、
riscv-gcc/gcc/common/config/riscv/riscv-common.c static const char * riscv_supported_std_ext (void) { return "mafdqlkcbjtpvn"; } ... static void riscv_parse_arch_string (const char *isa, int *flags, location_t loc) { ... if (subset_list->lookup ("d")) *flags |= MASK_DOUBLE_FLOAT; // add quantum *flags &= ~MASK_QUANTUM; if (subset_list->lookup ("k")) *flags |= MASK_QUANTUM; *flags &= ~MASK_RVC; if (subset_list->lookup ("c")) *flags |= MASK_RVC; ... }
riscv-gcc/gcc/config/riscv/riscv.opt
Mask(RVC)
Mask(RVE)
; add quantum
Mask(QUANTUM)
この MASK_QUANTUM がどこからやってくるか?というと、riscv-gcc/gcc/opth-gen.awk が riscv.opt を読み込んで自動生成、ビルド時に build-gcc-newlib-stage1/gcc/options.h, ./build-gcc-newlib-stage2/gcc/options.h に対して自動生成される。
ビルドしないと出てこない define なだけに、どこにあるのか?迷ってしまった…この辺は改善の余地があると思われ。
とりあえず、ここでは量子系の命令での特別な処理をgcc上では行わなくてもOKなので、gccの方はこれで終わり。次に実際の命令セットの追加をしていく。
3. add quantum registor&instruction set
次に量子用のレジスタと命令セットを追加していく。RISC-Vのレジスタと命令セットはコード的には一体となっていて、次の部分に修正を加えていく
riscv-binutils/opcodes/riscv-opc.c
const char * const riscv_gpr_names_abi[NGPR] = {
"zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2",
"s0", "s1", "a0", "a1", "a2", "a3", "a4", "a5",
"a6", "a7", "s2", "s3", "s4", "s5", "s6", "s7",
"s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6"
};
...
// quantum registor set
const char * const riscv_qpr_names_numeric[NFPR] =
{
"q0", "q1", "q2", "q3", "q4", "q5", "q6", "q7",
"q8", "q9", "q10", "q11", "q12", "q13", "q14", "q15",
"q16", "q17", "q18", "q19", "q20", "q21", "q22", "q23",
"q24", "q25", "q26", "q27", "q28", "q29", "q30", "q31"
};
const char * const riscv_qpr_names_abi[NFPR] = {
"qzero", "qt1", "qt2", "qt3", "qt4", "qt5", "qt6", "qt7",
"qs0", "qs1", "qa0", "qa1", "qa2", "qa3", "qa4", "qa5",
"qa6", "qa7", "qs2", "qs3", "qs4", "qs5", "qs6", "qs7",
"qs8", "qs9", "qs10", "qs11", "qt8", "qt9", "qt10", "qt11"
};
...
/* Quantum instruction subset */
{"qmeas.k", 0, {"K", 0}, "kD,kS,kT,ku", MATCH_QMEAS_K, MASK_QMEAS_K, match_opcode, 0},
{"qooh.k", 0, {"K", 0}, "kD,kS,kT,ku", MATCH_QOOH_K, MASK_QOOH_K, match_opcode, 0},
この riscv_qpr_names_abi がアセンブラコードに記述する量子用レジスタ名に相当するものになる。
riscv-binutils/include/opcode/riscv-opc.h
#define MATCH_QMEAS_K 0x8000000b
#define MASK_QMEAS_K 0x8000707f
#define MATCH_QOOH_K 0x700b
#define MASK_QOOH_K 0x8000707f
...
DECLARE_INSN(qmeas_k, MATCH_QMEAS_K, MASK_QMEAS_K)
DECLARE_INSN(qooh_k, MATCH_QOOH_K, MASK_QOOH_K)
これでとりあえずは定義類の準備はOK。次に命令セットを実行ファイルに組込む部分の実装を加えていく。
4. add quantum registor&instruction impl
上記の定義類を使って今度はアセンブリコードを実行ファイルに組込んでいく実装をしていく。ソースコード〜実行ファイルでマシン上で実行される流れは、自分的には「誰が読んで翻訳していくか?」といった物に近い。一般的なC/C++のソースコードは人が可視化・解釈しやすい文法表現になっていて、アセンブラはよりマシンに近い、最終的に実行ファイル&命令セットになるとマシンは簡単に解釈してくれる。それぞれの過程で、どの表現が誰にとって解釈しやすいか?翻訳する作業=コンパイルと考えていたりする。
ここで実装する部分は、フロントエンドが解釈&作ったアセンブラコードを実行ファイルにする実装になる。構文解釈はすっ飛ばしている。
riscv-binutils/gas/config/tc-riscv.c
void
md_begin (void)
{
...
// add quantum regitor to hash table.
hash_reg_names (RCLASS_FPR, riscv_fpr_names_abi, NFPR);
hash_reg_names (RCLASS_FPR, riscv_qpr_names_numeric, NFPR);
hash_reg_names (RCLASS_FPR, riscv_qpr_names_abi, NFPR);
...
}
riscv_set_arch (const char *s)
{
// add quantum = k
const char *all_subsets = "imafkdqc";
char *extension = NULL;
...
}
static const char *
riscv_ip (char *str, struct riscv_cl_insn *ip, expressionS *imm_expr,
bfd_reloc_code_real_type *imm_reloc, struct hash_control *hash)
...
for ( ; insn && insn->name && strcmp (insn->name, str) == 0; insn++){
...
for (args = insn->args;; ++args) {
...
switch (*args) {
...
// add RD/RS1/RS2/QIMM6 value
case 'k': /* Quantum */
switch (*++args) {
case 'D': /* Floating-point RD . */
// qmeas.k is special impl.
// reg_lookup is registor find func.
if (!((ip->insn_opcode ^ MATCH_QMEAS_K) & MASK_QMEAS_K)) {
if (!reg_lookup (&s, RCLASS_GPR, ®no)
|| !(regno >= 0 && regno <= 31))
break;
} else if (!reg_lookup (&s, RCLASS_FPR, ®no)
|| !(regno >= 0 && regno <= 31))
break;
// write regno to ip(RD)
INSERT_OPERAND (RD, *ip, regno);
continue;
case 'S': /* Floating-point RS1 x8-x15. */
if (!reg_lookup (&s, RCLASS_FPR, ®no)
|| !(regno >= 0 && regno <= 31))
break;
INSERT_OPERAND (RS1, *ip, regno);
continue;
case 'T': /* Floating-point RS2. */
if (!reg_lookup (&s, RCLASS_FPR, ®no))
break;
INSERT_OPERAND (RS2, *ip, regno);
continue;
case 'u': /* CUSTOM_IMM */
/* for qmeas.k high bits */
if (!((ip->insn_opcode ^ MATCH_QMEAS_K) & MASK_QMEAS_K)) {
INSERT_OPERAND (CUSTOM_IMM, *ip, atoi(s));
ip->insn_opcode |= MATCH_QMEAS_K;
} else
INSERT_OPERAND (CUSTOM_IMM, *ip, atoi(s));
s += strlen (s);
continue;
}
break;
...
}
意外とgcc側の改造もシンプルで簡単に行うことが出来た。これで一通りの実装はOK。”1. build RISC-V GNU Toolchain”に戻ってビルドをすればgccが出来上がる。
他にも実行ファイルをdumpする riscv-binutils/opcodes/riscv-dis.c の方にも修正を加えていくと、実行ファイルをアセンブリに戻して、上記の実装内容が正しいかどうか?確認することが出来る。
ということで、Hadamardして観測をSpike上でバンバン実行してみよう。
spike -q3 pk test
#qregister size : 32
#qbit size : 3
bbl loader
hadamard : rd: 10, rs1: 1, rs2: 0, qimm6: 1
qmeas : rd: 15, rs1: 1, rs2: 0, qimm6: 1
1
spike -q3 pk test
#qregister size : 32
#qbit size : 3
bbl loader
hadamard : rd: 10, rs1: 1, rs2: 0, qimm6: 1
qmeas : rd: 15, rs1: 1, rs2: 0, qimm6: 1
0
spike -q3 pk test
#qregister size : 32
#qbit size : 3
bbl loader
hadamard : rd: 10, rs1: 1, rs2: 0, qimm6: 1
qmeas : rd: 15, rs1: 1, rs2: 0, qimm6: 1
0
spike -q3 pk test
#qregister size : 32
#qbit size : 3
bbl loader
hadamard : rd: 10, rs1: 1, rs2: 0, qimm6: 1
qmeas : rd: 15, rs1: 1, rs2: 0, qimm6: 1
1
spike -q3 pk test
#qregister size : 32
#qbit size : 3
bbl loader
hadamard : rd: 10, rs1: 1, rs2: 0, qimm6: 1
qmeas : rd: 15, rs1: 1, rs2: 0, qimm6: 1
1
観測結果が0|1で入れ替わって良い感じに動いている。
とりあえず asm volatile を使ってアセンブラを直接実行をしているものの、この部分を関数として用意すると、量子系のプログラミング言語(Q#とか)を使わなくても通常のC/C++(gcc)を使って、超簡単に量子系の処理(古典・量子のマージ)をRISC-Vの拡張命令を使って行うことが出来た。
折角なので、次は量子系の言語をLLVMで作ってみよう。既に上記で用意したgcc toolchainのバックエンドを使って実行ファイルが作れるから、LLVMのフロントエンドを使うことで、表向きの言語はぶっちゃけ何でもOKになる。例えばBrainF*ckでも量子系のプログラムを行える。
1件の返信
[…] qlangというRISC-V 量子向けの独自のプログラミング言語をLLVM基盤上で開発していく。前にgccで行った改造内容から次のことが分かった。完全に同一のC言語のソースコードについて、 […]