Add Extend Instruction set to RISC-V GNU Toolchain

quantum computing

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, &regno)
                          || !(regno >= 0 && regno <= 31))
                          break;
                  } else if (!reg_lookup (&s, RCLASS_FPR, &regno)
                          || !(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, &regno)
                      || !(regno >= 0 && regno <= 31))
                    break;
                  INSERT_OPERAND (RS1, *ip, regno);
                  continue;
                case 'T': /* Floating-point RS2.  */
                  if (!reg_lookup (&s, RCLASS_FPR, &regno))
                    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件の返信

  1. […] qlangというRISC-V 量子向けの独自のプログラミング言語をLLVM基盤上で開発していく。前にgccで行った改造内容から次のことが分かった。完全に同一のC言語のソースコードについて、 […]

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です