Skip to content

Conversation

@rniczh
Copy link
Contributor

@rniczh rniczh commented Dec 15, 2025

Context:

Description of the Change:

This pipeline enables Catalyst to compile circuit for ARTIQ-based quantum device. When device_db configuration is provided, the compilation follows the ARTIQ route:

  • ions-decomposition: Decompose quantum operations for trapped ion systems
  • gates-to-pulses: Convert gate operations to pulse sequences
  • convert-ion-to-rtio: Convert ion operations to RTIO dialect
  • convert-rtio-event-to-artiq: Lower RTIO event to ARTIQ's primitives
  • llvm-dialect-lowering-stage: Lower to LLVM IR
  • emit-artiq-runtime: Generate ARTIQ runtime entry point as wrapper for ARITQ device to execute

The final stage compiles LLVM IR to an ELF binary targeting the ARTIQ device (ARM Cortex-A9). Due to current limitations where Catalyst's internal LLVM build does not include the corresponding ARM backend, the compilation will use llc to compile .ll to object file, and use ld.lld to compile to .elf

We added a compile_to_artiq helper function to the oqd module, so it can compile and link Catalyst-generated LLVM IR to ARTIQ's binary, keep OQD-specific logic out of Catalyst core.

Example:
Replace the artiq configs to your own env setting:

import os

import numpy as np
import pennylane as qml

from catalyst import qjit
from catalyst.third_party.oqd import OQDDevice, OQDDevicePipeline, compile_to_artiq

OQD_PIPELINES = OQDDevicePipeline(
    os.path.join("calibration_data", "device.toml"),
    os.path.join("calibration_data", "qubit.toml"),
    os.path.join("calibration_data", "gate.toml"),
    os.path.join("device_db", "device_db.json"),
)


def test_rx_gate():
    """Test RX gate with ARTIQ linking done in user code."""
    oqd_dev = OQDDevice(
        backend="default",
        shots=4,
        wires=1,
    )
    qml.capture.enable()

    # Compile to LLVM IR only
    @qjit(pipelines=OQD_PIPELINES, target="llvmir")
    @qml.qnode(oqd_dev)
    def circuit():
        x = np.pi / 2
        qml.RX(x, wires=0)
        return qml.counts(wires=0)

    # Compile to ARTIQ ELF
    artiq_config = {
        "kernel_ld": "/path/to/kernel.ld",
        "llc_path": "/path/to/llc",
        "lld_path": "/path/to/ld.lld",
    }

    output_elf_path = compile_to_artiq(circuit, artiq_config)
    print(f"ARTIQ ELF file generated: {output_elf_path}")

test_rx_gate()

The result will store to circuit.elf

[ARTIQ] Generated ELF: circuit.elf

And you can run it on artiq device:

artiq_run  --device-db device_db.py circuit.elf

Benefits:

Possible Drawbacks:
It causes the OQD specific compile stuffs steps into the original catalyst compiler driver's pipepline

Related GitHub Issues:
[sc-100853]

@github-actions
Copy link
Contributor

Hello. You may have forgotten to update the changelog!
Please edit doc/releases/changelog-dev.md on your branch with:

  • A one-to-two sentence description of the change. You may include a small working example for new features.
  • A link back to this PR.
  • Your name (or GitHub username) in the contributors section.

@rniczh rniczh requested a review from mehrdad2m January 15, 2026 18:29
@codecov
Copy link

codecov bot commented Jan 15, 2026

Codecov Report

❌ Patch coverage is 23.77049% with 93 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.62%. Comparing base (b3bcad1) to head (abbeb60).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
frontend/catalyst/third_party/oqd/oqd_compile.py 12.82% 68 Missing ⚠️
frontend/catalyst/jit.py 25.00% 9 Missing and 3 partials ⚠️
frontend/catalyst/third_party/oqd/oqd_device.py 57.89% 5 Missing and 3 partials ⚠️
frontend/catalyst/compiler.py 28.57% 4 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2299      +/-   ##
==========================================
- Coverage   97.31%   96.62%   -0.69%     
==========================================
  Files         107      108       +1     
  Lines       12951    13066     +115     
  Branches     1075     1100      +25     
==========================================
+ Hits        12603    12625      +22     
- Misses        288      374      +86     
- Partials       60       67       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Member

@paul0403 paul0403 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Hongsheng, looks really nice! I think this is the minimally intrusive way we can do it without altering too much of the base jit flow!

I left some comments but all are minor. Another thing is can you add your PR description's example as an end-to-end pytest in the frontend oqd tests? Obviously no need for execution, just check that the compiled ELF exist (maybe check a little bit of its content as well)? You may need to do things like subprocess.run("which llc", shell=True) in the test, but I think that's possible.

llvm_ir_path = os.path.join(str(circuit.workspace), f"{circuit_name}.ll")
with open(llvm_ir_path, "w", encoding="utf-8") as f:
f.write(llvm_ir_text)
print(f"LLVM IR file written to: {llvm_ir_path}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only print when verbose? Or should we always print?

print(f"LLVM IR file written to: {llvm_ir_path}")

# Link to ARTIQ's binary
if output_elf_name is None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this condition? 🤔 Is it for the user to specify a path they want themselves?

Comment on lines +110 to +111
"-mtriple=armv7-unknown-linux-gnueabihf",
"-mcpu=cortex-a9",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this constaint the type of the host machine? i.e. does this mean an OQD script can only be run on these kinds of systems?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the function compile_to_artiq is solely compile to ARTIQ and this is the hardware that ARTIQ employed. Well, it might be chnaged if the OQD target to different FPGA board (except the ARTIQ).

"-shared",
"--eh-frame-hdr",
"-m",
"armelf_linux_eabi",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if verbose:
print(f"[ARTIQ] Linking ELF: {' '.join(lld_args)}")

try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This try-except block seems identical for llc and lld. Maybe we can factor them out into a helper function? But I'm also fine with it if you leave it as is 👍


private:
/// Emit ARTIQ runtime wrapper for LLVM dialect kernel function
LogicalResult emitARTIQRuntimeForLLVMFunc(ModuleOp module, OpBuilder &builder,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

module is a keyword in cpp, let's avoid it as a variable name maybe

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing out!

Co-authored-by: Paul <79805239+paul0403@users.noreply.github.com>
Copy link
Contributor

@mehrdad2m mehrdad2m left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rniczh, I did a first round of review with some minor comments. However, I would like to play around with a bit more and maybe more review on Monday.

@@ -1,4 +1,4 @@
# Copyright 2024 Xanadu Quantum Technologies Inc.
# Copyright 2024-2026 Xanadu Quantum Technologies Inc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the copyright change? 😄

@@ -1,4 +1,4 @@
# Copyright 2024 Xanadu Quantum Technologies Inc.
# Copyright 2024-2026 Xanadu Quantum Technologies Inc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

@@ -1,4 +1,4 @@
# Copyright 2022-2023 Xanadu Quantum Technologies Inc.
# Copyright 2022-2026 Xanadu Quantum Technologies Inc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Co-authored-by: Mehrdad Malek <39844030+mehrdad2m@users.noreply.github.com>
rniczh and others added 2 commits January 16, 2026 17:13
Co-authored-by: Mehrdad Malek <39844030+mehrdad2m@users.noreply.github.com>
Co-authored-by: Mehrdad Malek <39844030+mehrdad2m@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants