Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions frontend/catalyst/compiler.py
Original file line number Diff line number Diff line change
@@ -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


# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -430,6 +430,7 @@ def get_cli_command(self, tmp_infile_name, output_ir_name, module_name, workspac
)
return cmd

# pylint: disable=too-many-branches
@debug_logger
def run_from_ir(self, ir: str, module_name: str, workspace: Directory):
"""Compile a shared object from a textual IR (MLIR or LLVM).
Expand Down Expand Up @@ -484,16 +485,23 @@ def run_from_ir(self, ir: str, module_name: str, workspace: Directory):
else:
out_IR = None

# If target is llvm-ir, only return LLVM IR without linking
if self.options.target == "llvmir":
output = output_ir_name if os.path.exists(output_ir_name) else None
if os.path.exists(tmp_infile_name):
os.remove(tmp_infile_name)
return output, out_IR

output = LinkerDriver.run(output_object_name, options=self.options)
output_object_name = str(pathlib.Path(output).absolute())
output = str(pathlib.Path(output).absolute())

# Clean up temporary files
if os.path.exists(tmp_infile_name):
os.remove(tmp_infile_name)
if os.path.exists(output_ir_name):
os.remove(output_ir_name)

return output_object_name, out_IR
return output, out_IR

def has_xdsl_passes_in_transform_modules(self, mlir_module):
"""Check if the MLIR module contains xDSL passes in transform dialect.
Expand Down
27 changes: 23 additions & 4 deletions frontend/catalyst/jit.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2022-2024 Xanadu Quantum Technologies Inc.
# Copyright 2022-2026 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -603,6 +603,10 @@ def __call__(self, *args, **kwargs):

requires_promotion = self.jit_compile(args, **kwargs)

# For llvm-ir target, compilation is complete, no execution needed
if self.compile_options.target == "llvmir":
return None

# If we receive tracers as input, dispatch to the JAX integration.
if any(isinstance(arg, jax.core.Tracer) for arg in tree_flatten(args)[0]):
if self.jaxed_function is None:
Expand All @@ -621,16 +625,18 @@ def aot_compile(self):
self.workspace = self._get_workspace()

# TODO: awkward, refactor or redesign the target feature
if self.compile_options.target in ("jaxpr", "mlir", "binary"):
if self.compile_options.target in ("jaxpr", "mlir", "llvmir", "binary"):
self.jaxpr, self.out_type, self.out_treedef, self.c_sig = self.capture(
self.user_sig or ()
)

if self.compile_options.target in ("mlir", "binary"):
if self.compile_options.target in ("mlir", "llvmir", "binary"):
self.mlir_module = self.generate_ir()

if self.compile_options.target in ("binary",):
if self.compile_options.target in ("llvmir", "binary"):
self.compiled_function, _ = self.compile()

if self.compile_options.target in ("binary",):
self.fn_cache.insert(
self.compiled_function, self.user_sig, self.out_treedef, self.workspace
)
Expand All @@ -656,6 +662,15 @@ def jit_compile(self, args, **kwargs):
bool: whether the provided arguments will require promotion to be used with the compiled
function
"""
if self.compile_options.target == "llvmir":
if self.mlir_module is not None:
return False
self.workspace = self._get_workspace()
self.jaxpr, self.out_type, self.out_treedef, self.c_sig = self.capture(args, **kwargs)
self.mlir_module = self.generate_ir()
self.compiled_function, _ = self.compile()
return False

cached_fn, requires_promotion = self.fn_cache.lookup(args)

if cached_fn is None:
Expand Down Expand Up @@ -795,6 +810,7 @@ def compile(self):

Returns:
Tuple[CompiledFunction, str]: the compilation result and LLVMIR
For targets that skip execution, returns (None, llvm_ir) instead.
"""
# WARNING: assumption is that the first function is the entry point to the compiled program.
entry_point_func = self.mlir_module.body.operations[0]
Expand All @@ -820,6 +836,9 @@ def compile(self):
else:
shared_object, llvm_ir = self.compiler.run(self.mlir_module, self.workspace)

if self.compile_options.target == "llvmir":
return None, llvm_ir

compiled_fn = CompiledFunction(
shared_object, func_name, restype, self.out_type, self.compile_options
)
Expand Down
5 changes: 3 additions & 2 deletions frontend/catalyst/third_party/oqd/__init__.py
Original file line number Diff line number Diff line 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.

Why the copyright change? 😄


# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -15,6 +15,7 @@
This submodule contains classes for the OQD device and its properties.
"""

from .oqd_compile import compile_to_artiq
from .oqd_device import OQDDevice, OQDDevicePipeline

__all__ = ["OQDDevice", "OQDDevicePipeline"]
__all__ = ["OQDDevice", "OQDDevicePipeline", "compile_to_artiq"]
229 changes: 229 additions & 0 deletions frontend/catalyst/third_party/oqd/oqd_compile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# Copyright 2026 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
OQD Compiler utilities for compiling and linking LLVM IR to ARTIQ's binary.
"""

import os
import subprocess
from pathlib import Path
from typing import Optional


def compile_to_artiq(circuit, artiq_config, output_elf_name=None, verbose=True):
"""Compile a qjit-compiled circuit to ARTIQ's binary.

This function takes a circuit compiled with target="llvmir", writes the LLVM IR
to a file, and links it to an ARTIQ's binary.

Args:
circuit: A QJIT-compiled function (must be compiled with target="llvmir")
artiq_config: Dictionary containing ARTIQ configuration:
- kernel_ld: Path to ARTIQ kernel linker script
- llc_path: (optional) Path to llc compiler
- lld_path: (optional) Path to ld.lld linker
output_elf_name: Name of the output ELF file (default: None, uses circuit function name)
verbose: Whether to print verbose output (default: True)

Returns:
str: Path to the generated binary file
"""
# Get LLVM IR text and write to file
llvm_ir_text = circuit.qir
circuit_name = getattr(circuit, "__name__", "circuit")
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?


# 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?

output_elf_name = f"{circuit_name}.elf"

# Output ELF file to current working directory if workspace is in /private (temp dir),
# otherwise use workspace directory
workspace_str = str(circuit.workspace)
if "/private" in workspace_str:
output_elf_path = os.path.join(os.getcwd(), output_elf_name)
else:
output_elf_path = os.path.join(workspace_str, output_elf_name)

link_to_artiq_elf(
llvm_ir_path=llvm_ir_path,
output_elf_path=output_elf_path,
kernel_ld=artiq_config["kernel_ld"],
llc_path=artiq_config.get("llc_path"),
lld_path=artiq_config.get("lld_path"),
verbose=verbose,
)

return output_elf_path


def _validate_paths(llvm_ir_path: Path, kernel_ld: Path) -> None:
"""Validate that required input files exist."""
if not llvm_ir_path.exists():
raise FileNotFoundError(f"LLVM IR file not found: {llvm_ir_path}")
if not kernel_ld.exists():
raise FileNotFoundError(f"ARTIQ kernel.ld not found: {kernel_ld}")


def _get_tool_command(tool_path: Optional[str], default_name: str) -> str:
"""Get tool command path, validating if custom path is provided."""
if tool_path is None:
return default_name
tool_path_obj = Path(tool_path)
if not tool_path_obj.exists():
raise FileNotFoundError(f"{default_name} not found: {tool_path}")
return tool_path


def _compile_llvm_to_object(
llvm_ir_path: Path, object_file: Path, llc_cmd: str, verbose: bool
) -> None:
"""Compile LLVM IR to object file with llc.

Args:
llvm_ir_path: Path to LLVM IR file
object_file: Path to object file
llc_cmd: Command to use for llc compiler
verbose: Whether to print verbose output

Raises:
RuntimeError: If compilation fails
FileNotFoundError: If llc is not found
"""
llc_args = [
llc_cmd,
"-mtriple=armv7-unknown-linux-gnueabihf",
"-mcpu=cortex-a9",
Comment on lines +110 to +111
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).

"-filetype=obj",
"-relocation-model=pic",
"-o",
str(object_file),
str(llvm_ir_path),
]

if verbose:
print(f"[ARTIQ] Compiling with external LLC: {' '.join(llc_args)}")

try:
result = subprocess.run(llc_args, check=True, capture_output=True, text=True)
if verbose and result.stderr:
print(f"[ARTIQ] LLC stderr: {result.stderr}")
except subprocess.CalledProcessError as e:
error_msg = f"External LLC failed with exit code: {e.returncode}"
if e.stderr:
error_msg += f"\n{e.stderr}"
raise RuntimeError(error_msg) from e
except FileNotFoundError as exc:
raise FileNotFoundError(
"llc not found. Please install LLVM or provide path via llc_path argument."
) from exc

if not object_file.exists():
raise RuntimeError(f"Object file was not created: {object_file}")


def _link_object_to_elf(
object_file: Path, output_elf_path: Path, kernel_ld: Path, lld_cmd: str, verbose: bool
) -> None:
"""Link object file to ELF format with ld.lld.

Args:
object_file: Path to object file
output_elf_path: Path to output ELF file
kernel_ld: Path to kernel linker script
lld_cmd: Command to use for ld.lld linker
verbose: Whether to print verbose output

Raises:
RuntimeError: If linking fails
FileNotFoundError: If ld.lld is not found
"""
lld_args = [
lld_cmd,
"-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.

"--target2=rel",
"-T",
str(kernel_ld),
str(object_file),
"-o",
str(output_elf_path),
]

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 👍

result = subprocess.run(lld_args, check=True, capture_output=True, text=True)
if verbose and result.stderr:
print(f"[ARTIQ] LLD stderr: {result.stderr}")
except subprocess.CalledProcessError as e:
error_msg = f"LLD linking failed with exit code: {e.returncode}"
if e.stderr:
error_msg += f"\n{e.stderr}"
raise RuntimeError(error_msg) from e
except FileNotFoundError as exc:
raise FileNotFoundError(
"ld.lld not found. Please install LLVM LLD or provide path via lld_path argument."
) from exc

if not output_elf_path.exists():
raise RuntimeError(f"ELF file was not created: {output_elf_path}")


# pylint: disable=too-many-arguments,too-many-positional-arguments
def link_to_artiq_elf(
llvm_ir_path: str,
output_elf_path: str,
kernel_ld: str,
llc_path: Optional[str] = None,
lld_path: Optional[str] = None,
verbose: bool = False,
) -> str:
"""Link LLVM IR to ARTIQ ELF format.

Args:
llvm_ir_path: Path to the LLVM IR file (.ll)
output_elf_path: Path to output ELF file
kernel_ld: Path to ARTIQ's kernel.ld linker script
llc_path: Path to llc (LLVM compiler). If None, uses "llc" from PATH
lld_path: Path to ld.lld (LLVM linker). If None, uses "ld.lld" from PATH
verbose: If True, print compilation commands

Returns:
Path to the generated ELF file
"""
llvm_ir_path_obj = Path(llvm_ir_path)
output_elf_path_obj = Path(output_elf_path)
kernel_ld_obj = Path(kernel_ld)

_validate_paths(llvm_ir_path_obj, kernel_ld_obj)

llc_cmd = _get_tool_command(llc_path, "llc")
lld_cmd = _get_tool_command(lld_path, "ld.lld")

object_file = output_elf_path_obj.with_suffix(".o")
_compile_llvm_to_object(llvm_ir_path_obj, object_file, llc_cmd, verbose)
_link_object_to_elf(object_file, output_elf_path_obj, kernel_ld_obj, lld_cmd, verbose)

if verbose:
print(f"[ARTIQ] Generated ELF: {output_elf_path_obj}")

return str(output_elf_path_obj)
Loading