量子コンピューターアドベントカレンダーの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;
}
if (res == One) {
set numOnes += 1;
}
}
Set(Zero, q0);
Set(Zero, q1);
}
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: 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
Globals:
Function:
Timeout: 10
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./src/HelloWorld/
Handler: HelloWorld::HelloWorld.Function::FunctionHandler
Runtime: dotnetcore2.1
Environment:
Variables:
NEW_RELIC_ACCOUNT_ID: 307596
NEW_RELIC_TRUSTED_ACCOUNT_KEY: 307596
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: get
Outputs:
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で監視していきましょう。手順はここに載っています。
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
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によるもので、量子シミュレーション部分にかかる時間はパラメーター(特に試行回数)が同じ限りはほぼ同じというところまでわかります。
というわけでかなりの変化球ネタでしたが、量子コンピューターのシミュレーターもサーバーレスで動かせることができるし、そのモニタリングも可能です、というご紹介でした。