量子コンピューターアドベントカレンダーの12日目のエントリです。
何か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も使えます。
次にQ#コードですが、今回はQ#の最初のチュートリアルのBell状態のテストのコードを使ってみます。
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#コードが実行されて結果が帰ってきます。
Q#が動いている.NET Core LambdaをNew Relicで監視する
というところまでがひとまずLambdaでのQ#コードの実行です。ここからNew Relicで監視していきましょう。手順はここに載っています。
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のメトリクスが取れますか、より詳細なトレースが取得できるようになります。
またトレースの情報は分散トレーシングとして表示できます。分散トレーシングでは一番大元のRootSpanから、スタックトーレスのように処理を小分けしてSpanが並んでいきます。ここでは大元のRootSpanと次のFunctionHandlerを見ると全体が2.32秒かかっていることがわかります。その中でTestBellState.Runという量子シミュレーター部分は980msec(ほぼ1秒)ということまでわかりました。
さらにAttributesには選択したSpanの詳細が表示されます。コードでセットしたAllocatedというタグの値も表示され、今回は2量子ビット割り当てられたことがわかります。
また時系列でSpanごとにかかった時間も見られるため、時々遅くなるのはLambdaのColdStartによるもので、量子シミュレーション部分にかかる時間はパラメーター(特に試行回数)が同じ限りはほぼ同じというところまでわかります。
というわけでかなりの変化球ネタでしたが、量子コンピューターのシミュレーターもサーバーレスで動かせることができるし、そのモニタリングも可能です、というご紹介でした。