銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

Q#のコードをAWS Lambdaでサーバーレス実行して、New Relicで監視する

量子コンピューターアドベントカレンダーの12日目のエントリです。

qiita.com

何かQ#のエントリ入れたいなあと思いつつ、いいネタがなかったので変化球で攻めたいと思います。Azure Quantumがまだ公開されていない中、AWS Bracketがリリースされたわけですが、そこでお手軽にQ#の量子シミュレーターを使えるように、AWS Lambdaで動かしてみようと思います。API Gatewayを使えばURLを叩くだけでQ#で書いた量子シミュレーターを起動できます。 で、ただサーバーレスで動かすだけだと実行回数や経過時間などがみられないので、これをNew Relicを使って監視してみたいと思います。

AWS LambdaでQ#を動かす

Q#がAWS Lambdaで動くのか?という疑問ですが、動きます。Q#のコードはC#コードにトランスパイルされた上でC#コードとしてビルドされて.NET Core Runtime上で動きます。つまり、Q#のコードを実行するプログラムは、Q#用の単なる.NET Coreのライブラリを含む通常の.NET Coreのバイナリです。そしてAWS Lambdaは.NET Core 2.1をサポートしているので、.NET Core 3.1ではなく2.1 を使えば動かせます。

今回、.NET CoreのLambdaのプロジェクトはAWS SAMを使って雛形を出力し、Riderで開発しています。が、基本的には普通の.NET Coreプロジェクトです。AWS Lambdaに必要なライブラリとQ#のライブラリを追加するとcsprojはこのようになっています。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Amazon.Lambda.Core" Version="1.1.0" />
    <PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="1.2.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.Json" Version="1.5.0" />
    <PackageReference Include="Microsoft.Quantum.Development.Kit" Version="0.9.1909.3002" />
    <PackageReference Include="Microsoft.Quantum.Simulators" Version="0.9.1909.3002" />
    <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
  </ItemGroup>

  <ItemGroup>
    <Content Include="HelloQuantum.qs" />
  </ItemGroup>
  
</Project>

注意点として、Q#の最新版は0.10ですが、0.10は.NET Core 3.0以上が必要なので.NET Core 2.1で動く0.9にしています。AWS LambdaではLTS版の3.1をサポート予定とのことですので、もうしばらく待つかカスタムイメージを使うとQ# 0.10も使えます。

aws.amazon.com

次にQ#コードですが、今回はQ#の最初のチュートリアルのBell状態のテストのコードを使ってみます。

docs.microsoft.com

namespace HelloWorld {
    open Microsoft.Quantum.Convert;
    open Microsoft.Quantum.Intrinsic;

    operation Set(desired : Result, q1 : Qubit) : Unit {
        if (desired != M(q1)) {
            X(q1);
        }
    }
    
    
    operation TestBellState(count : Int, initial : Result) : (Int, Int, Int) {
            mutable numOnes = 0;
            mutable agree = 0;
            using ((q0, q1) = (Qubit(), Qubit())) {
                Message($"Looping {count} times...");
                for (test in 1..count) {
                    Set(initial, q0);
                    Set(Zero, q1);
    
                    H(q0);
                    CNOT(q0, q1);
                    let res = M(q0);
    
                    if (M(q1) == res) {
                        set agree += 1;
                    }
                    // Count the number of ones we saw:
                    if (res == One) {
                        set numOnes += 1;
                    }
                }
                
                Set(Zero, q0);
                Set(Zero, q1);
            }
    
            // Return number of times we saw a |0> and number of times we saw a |1>
            return (count-numOnes, numOnes, agree);
        }
}

そしてLambdaで実行されるコードからこのQ#のコードを呼び出してみます。今回はAPIGateway経由での呼び出しを想定しているので、FunctionHandlerの引数がAPIGatewayProxyRequest型になっており、そこから思考回数と初期値を取得するようにしてみました。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Net.Http;
using Newtonsoft.Json;

using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;
using Microsoft.Quantum.Simulation.Core;
using Microsoft.Quantum.Simulation.Simulators;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]
namespace HelloWorld
{
    public class Function
    {
        public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apiGatewayProxyRequest, ILambdaContext context)
        {
            using (var qsim = new QuantumSimulator())
            {
                if (!(apiGatewayProxyRequest.QueryStringParameters.TryGetValue("count", out var countAsStr) &&
                        int.TryParse(countAsStr, out var count)))
                    count = 1000;
                var initial = Result.Zero;
                if (!(apiGatewayProxyRequest.QueryStringParameters.TryGetValue("initial", out var initialAsStr) &&
                        bool.TryParse(initialAsStr, out var initialAsBool) && initialAsBool))
                    initial = Result.One;

                var (numZeros, numOnes, agrees) = TestBellState.Run(qsim, count, initial).GetAwaiter().GetResult();
                
                var body = new Dictionary<string, long>
                {
                    { "numZeros", numZeros },
                    { "numOnes", numOnes },
                    { "agrees", agrees},
                };

                return new APIGatewayProxyResponse
                {
                    Body = JsonConvert.SerializeObject(body),
                    StatusCode = 200,
                    Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
                };
            }
            
        }
    }
}

AWS SAMからテンプレートを作った場合、template.yamlはこんな風になっています。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Sample SAM Template for AWS

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 10

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: ./src/HelloWorld/
      Handler: HelloWorld::HelloWorld.Function::FunctionHandler
      Runtime: dotnetcore2.1
      Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
        Variables:
          NEW_RELIC_ACCOUNT_ID: 307596
          NEW_RELIC_TRUSTED_ACCOUNT_KEY: 307596
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

これで、プロジェクトをビルドしてパッケージして展開すると利用できるようになります。READMEに記述されていますが、以下のコマンドを順に実行するとデプロイが完了します。BUCKET_NAMEには作業用に作ったS3のバケットを指定します。

$ sam build
$ sam package \
    --output-template-file packaged.yaml \
    --s3-bucket <BUCKET_NAME>
$ sam deploy \
    --template-file packaged.yaml \
    --stack-name AWS \
    --capabilities CAPABILITY_IAM

最初にデプロイした場合は、次のコマンドで作成されたAPI Gatewayのエンドポイントを表示します。

$ aws cloudformation describe-stacks \
    --stack-name AWS \
    --query 'Stacks[].Outputs[?OutputKey==`HelloWorldApi`]' \
    --output table

表示されたURLに適当にクエリパラメーターをつけてアクセスすると無事にQ#コードが実行されて結果が帰ってきます。

f:id:tanaka733:20191209235554j:plain

Q#が動いている.NET Core LambdaをNew Relicで監視する

というところまでがひとまずLambdaでのQ#コードの実行です。ここからNew Relicで監視していきましょう。手順はここに載っています。

docs.newrelic.com

newrelic-lambda-cliをインストールして手順通りにセットアップしていきます。

$ pip3 install newrelic-lambda-cli
$  export AWS_DEFAULT_REGION=ap-northeast-1
$ newrelic-lambda integrations install --nr-account-id <New RelicアカウントID> \
--linked-account-name <AWS Integrationでリンクした名前> \
--nr-api-key <API_KEY>
$ newrelic-lambda subscriptions install --function <作成したFUNCTION名>

加えてLambdaのコードにも少し手を入れます。まずNew Relic Agentのライブラリ参照をNuGet経由で追加します。

    <PackageReference Include="NewRelic.OpenTracing.AmazonLambda.Tracer" Version="1.0.0" />

ドキュメントにしたがって、Tracerの初期化を行い、ラッパー関数を新たに定義します。

using NewRelic.OpenTracing.AmazonLambda;
using OpenTracing.Util;

        static Function()
        {
            // Register The NewRelic Lambda Tracer Instance
            GlobalTracer.Register(NewRelic.OpenTracing.AmazonLambda.LambdaTracer.Instance);
        }

        public APIGatewayProxyResponse FunctionWrapper(APIGatewayProxyRequest apiGatewayProxyRequest, ILambdaContext context)
        {
            // Instantiate NewRelic TracingWrapper and pass your FunctionHandler as 
            // an argument
            return new TracingRequestHandler().LambdaWrapper(FunctionHandler, apiGatewayProxyRequest, context);
        }

Lambdaのエントリポイントを新しく作ったFunctionWrapperに変更します。

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: ./src/HelloWorld/
      Handler: HelloWorld::HelloWorld.Function::FunctionWrapper #<==この行を変更
#以下略

これだけで初期手順としては完了です。今回はさらに、Q#の呼び出し部分を抜き出して計測することにしましょう。分散トレーシングでこの部分にSpanを追加します。さらにSpanにはTagという追加情報を設定できますが、シミュレーターの種類(名前)と、割り当てられたQubitの数を追加してみましょう。割り当てられたQubitの数は明示的には取得できなそうなため、QuantumSimulatorクラスに用意されているOnAllocateQubitsイベントを購読してカウントアップするようにしました。

                var span = LambdaTracer.Instance.BuildSpan("TestBellState.Run").Start();
                span.SetTag("Simulator", qsim.Name);
                var qubits = 0l;
                qsim.OnAllocateQubits += (l) => { qubits += l; };
                var (numZeros, numOnes, agrees) = TestBellState.Run(qsim, count, initial).GetAwaiter().GetResult();
                span.SetTag("Allocated", qubits);
                span.Finish();

New RelicのLambdaダッシュボードはこのように表示されます。AWS CloudWatchでもある程度Lambdaのメトリクスが取れますか、より詳細なトレースが取得できるようになります。

f:id:tanaka733:20191211234131p:plain

またトレースの情報は分散トレーシングとして表示できます。分散トレーシングでは一番大元のRootSpanから、スタックトーレスのように処理を小分けしてSpanが並んでいきます。ここでは大元のRootSpanと次のFunctionHandlerを見ると全体が2.32秒かかっていることがわかります。その中でTestBellState.Runという量子シミュレーター部分は980msec(ほぼ1秒)ということまでわかりました。

f:id:tanaka733:20191210000021p:plain

さらにAttributesには選択したSpanの詳細が表示されます。コードでセットしたAllocatedというタグの値も表示され、今回は2量子ビット割り当てられたことがわかります。

f:id:tanaka733:20191210000049p:plain

また時系列でSpanごとにかかった時間も見られるため、時々遅くなるのはLambdaのColdStartによるもので、量子シミュレーション部分にかかる時間はパラメーター(特に試行回数)が同じ限りはほぼ同じというところまでわかります。

f:id:tanaka733:20191210000109p:plain

というわけでかなりの変化球ネタでしたが、量子コンピューターのシミュレーターもサーバーレスで動かせることができるし、そのモニタリングも可能です、というご紹介でした。