Hybrid Quantum Applications with Azure Functions

Holger

Hybrid Quantum Applications with Azure Functions

Quantum computers harness the unique behavior of quantum physics and apply it to computing. This approach promises massive speedup in compute time compared to classical computing, especially in areas like optimization, simulation, or machine learning. However, quantum computing components have a different operating model compared to classical software. There are typically one or more classical compute components that orchestrate the execution of quantum components. Azure Functions are one option for implementing these classical parts. This blog shows how Hybrid Quantum Applications with Azure Functions provide an easy way for exposing quantum functionality via APIs making it easy to access from classical code.

Quantum job orchestration includes following activities:

  • Preparation of classical input data
  • Submission of quantum computing jobs to a target quantum environment
  • Monitoring of the job execution
  • Post-processing of job results

If you are a quantum developer and implement the classic computation part yourself, the QDK provides everything you need to develop and use quantum algorithms. There is an excellent post by my esteemed colleague Mariia Mykhailova describing this quantum software development approach. In many cases, the result will be a hybrid quantum-classical software as described by my colleague Guen Prawiroatmodjo.

Integrate Quantum Functionality via an API

However, sooner or later you’ll want to make your quantum functionality available to non-quantum developers. For example, these developers might develop a classical web frontend for business users, which uses your quantum functionality to make some calculations on input data. And these developers might not want to install a QDK nor are they necessarily familiar with the CLI or Jupyter notebooks. At the same time, you’d like to further evolve your quantum code and still make it accessible.

The best way to loosely couple your quantum code with non-quantum software components is via an API. This way, classical developers can just call this API without worrying about implementation or runtime details of your quantum components. Azure Functions have some tempting qualities that make them an ideal tool for this purpose:

  • They represent a serverless solution. Compute resources are only provisioned and used when needed. This minimizes cost overhead.
  • The programming model is simple. You can focus on the core functionality (in our case the orchestration of the quantum job) and don’t need to worry about underlying infrastructure.
  • Functions can be called via http-requests. If required, a caller must authenticate properly before accessing the API-endpoint.

Architecture of Hybrid Quantum Applications with Azure Functions

The architecture for hybrid quantum applications with Azure Functions could look like as follows.

"Architecture

A signed-in user of a classical client application calls a functionality that requires a quantum job to be executed. The classic client calls the custom job-API for submitting the job. The API Gateway triggers an Azure Function passing job input data. The Azure Function puts input data into Azure Storage and submits the job to an Azure Quantum workspace and specifies the execution target(s). The function authenticates to the Quantum Workspace via managed identity. A provider executes the job on a target environment. The client application monitors job execution by polling job status via additional API-calls to a second Azure Function. When the job finishes, the compute results are stored in Azure Storage. The client application gets the results using an API that is implemented via the Azure Function.

Sample implementation

The following sample code implements the components contained in the architecture but needs to be treated carefully. The job submission and monitoring methods contained in the QDK libraries might get changed in future.

The quantum job written in Q#

First, create an empty Q#-program as described in the Azure Quantum quickstart. Let’s keep it simple and use a quantum number generation algorithm. It receives an integer input (the bit-length of the desired result) and produces a corresponding array of random bits. The code implementing this should look as follows:

namespace QuantumRNG {
  open Microsoft.Quantum.Intrinsic;
  open Microsoft.Quantum.Measurement;
  open Microsoft.Quantum.Canon;

  @EntryPoint()
  operation GenerateRandomBits(n : Int) : Result[] {
    use qubits = Qubit[n];
    ApplyToEach(H, qubits);
    return MultiM(qubits);
  }
}

When creating this Q# program, make sure you define it as an executable and that you annotate the bits generation operation with the @EntryPoint attribute. Otherwise, the Q# compiler won’t produce the metadata the classical functions need.

The Job Submission Function

You’ll implement the classical parts as Azure Functions. To get started on how to create new Functions have a look at the Azure Functions quickstarts.

The first function needed is the one that submits the quantum job to an Azure Quantum workspace.

Connecting the Function with the Quantum Job Definition

To be able to reference the quantum namespace, make sure that you reference the Q#-project in your Function-project file as a .NET project reference. In our case the .csproj-file could look as follows.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.13" />
    <PackageReference Include="Microsoft.Azure.Quantum.Client" Version="0.21.2111177148" />
    <PackageReference Include="Microsoft.Quantum.Providers.IonQ" Version="0.21.2111177148" />
    <ProjectReference Include="..\QApp.Qsharp\QApp.Qsharp.csproj" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

Implementation of the Job Submitting Function

You can implement the function as follows.

using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Quantum.Providers.IonQ.Targets;
using QuantumRNG;
using System;
using System.Threading.Tasks;

namespace Function.Csharp
{
  public static class CreateRandomNumberJob
  {
    [FunctionName("CreateRandomNumberJob")]
    public static async Task<IActionResult> Run(
      [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
      ILogger log)
    {
      // define the workspace that manages the target machines.
      var workspace = new Workspace
      (
        subscriptionId: Environment.GetEnvironmentVariable("subscriptionId"),
        resourceGroupName: Environment.GetEnvironmentVariable("resourceGroupName"),
        workspaceName: Environment.GetEnvironmentVariable("workspaceName"),
        location: Environment.GetEnvironmentVariable("location")
      );

      // Select the machine that will execute the Q# operation.
      // Target must be enabled in the workspace referenced above.
      var targetId = "ionq.simulator";

      var quantumMachine = new IonQQuantumMachine(
        target: targetId,
        workspace: workspace);

      // Submit the random number job to the target machine.
      var randomNumberJob = quantumMachine.SubmitAsync(GenerateRandomBits.Info, 4);
      return new OkObjectResult($"RandomBits job id: {randomNumberJob.Result.Id}");
    }
  }
}

Connecting the Function with the Quantum Workspace

Make sure that you specify the environment variables that define the quantum workspace properly. In Azure you can edit the Azure Function App configuration. For your local environment you can do this by specifying variables in your local.settings.json file as follows:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "subscriptionId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "resourceGroupName": "XXXX",
    "workspaceName": "XXXX",
    "location": "westeurope"
  }
}

After successful submission the Function returns the Job-ID that you can use to call the status function (to retrieve the job status or – if successfully finished – the job result).

The Job Status Function

You can use the second Function to query the job status. It connects to the workspace to get this information. After the job has finished (job status equals ‘succeeded’) it returns the result.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Quantum;
using Microsoft.Azure.Quantum.Storage;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Threading.Tasks;

namespace Function.Csharp
{
  public static class GetRandomNumberJob
  {
    [FunctionName("GetRandomNumberJob")]
    public static async Task<IActionResult> Run(
      [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
      ILogger log)
    {
      // define the workspace that manages the target machines.
      var workspace = new Workspace
      (
        subscriptionId: Environment.GetEnvironmentVariable("subscriptionId"),
        resourceGroupName: Environment.GetEnvironmentVariable("resourceGroupName"),
        workspaceName: Environment.GetEnvironmentVariable("workspaceName"),
        location: Environment.GetEnvironmentVariable("location")
      );

      var jobStorageHelper = new LinkedStorageJobHelper(workspace);

      string jobId = req.Query["jobId"];
      string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
      dynamic data = JsonConvert.DeserializeObject(requestBody);
      jobId = jobId ?? data?.jobId;

      // Get the job details from the workspace.
      var job = workspace.GetJob(jobId);
      var status = job.Details.Status;

      // If the job is in 'succeeded' state, download and return the results
      if (job.Succeeded)
      {
        string result;
        using (var stream = new MemoryStream())
        {
          jobStorageHelper.DownloadJobOutputAsync(jobId, stream).Wait();
          stream.Seek(0, SeekOrigin.Begin);
          result = new StreamReader(stream).ReadToEnd();
        }

        return new OkObjectResult($"Job {jobId} result: {result}");
      }
      // otherwise it just prints the job status.
      return new OkObjectResult($"Job {jobId} status: {status}");
    }
  }
}

Testing your Azure Functions

If everything compiles, you are ready to deploy your code to Azure and test it. Make sure that you’ve configured the workspace references in your Azure Function environment variables. Grant your Azure Function rights to call the workspace. Finally, you are ready to test the functions. All you need, is a client that can call an http-endpoint (for example, Postman). The easiest way is to just enter the Function Url in a browser:

https://<YOUR_FUNCTION_APP>.azurewebsites.net/api/CreateRandomNumberJob

This should return a job id:

RandomBits job id: 7e97fd4c-af5f-44f7-bf5b-d88961441bf8

Now, retrieve the job result by calling the second function and passing this job id as a parameter:

https:// <YOUR_FUNCTION_APP>.azurewebsites.net/api/GetRandomNumberJob?JobId=7e97fd4c-af5f-44f7-bf5b-d88961441bf8

This should return the measurement result. In case of the IonQ-simulator this will be histogram information:

Job 7e97fd4c-af5f-44f7-bf5b-d88961441bf8 result:
{"duration":12572947,"histogram":{"0":0.0625,"1":0.0625,"2":0.0625,"3":0.0625,"4":0.0625,"5":0.0625,"6":0.0625,"7":0.0625,"8":0.0625,"9":0.0625,"10":0.0625,"11":0.0625,"12":0.0625,"13":0.0625,"14":0.0625,"15":0.0625}}

Conclusion

Quantum applications are typically of hybrid nature: there is always a classical component that submits and monitors the quantum jobs at runtime. The classical component can be a CLI script or Jupyter Notebook. A more scalable approach is the packaging of quantum jobs in Azure Functions. They implement a serverless way of making functionality – in our case a quantum job – available and callable via RESTful-API. This way you can make your quantum functionality implemented in Q# available to non-quantum developers who just need to call the API and don’t need special quantum development skills.

The sourcecode of the overall solution is available on GitHub.

Posted in Q#

0 comments

Leave a comment