Skip to content

Added Wrapper Function to Infer Kernel Qubit Count#765

Open
jasonhan3 wants to merge 3 commits into
mainfrom
jasonh/331-wrap-kernel
Open

Added Wrapper Function to Infer Kernel Qubit Count#765
jasonhan3 wants to merge 3 commits into
mainfrom
jasonh/331-wrap-kernel

Conversation

@jasonhan3
Copy link
Copy Markdown
Contributor

@jasonhan3 jasonhan3 commented Apr 29, 2026

Summary

Closes #331.

** NOTE: this ONLY works for signatures that are of the form

def kernel(*qubits:Qubit, **kwargs)

and the qubits CANNOT be supplied as kwargs. Otherwise, this implementation will not work.**

This PR adds squin.wrap, a small utility for turning a qubit-operating kernel into a simulation-ready entry point. The generated wrapper allocates qubits, invokes the provided kernel, measures all allocated qubits, and returns the measurement results.

Example:

from bloqade import squin
from bloqade.types import Qubit


@squin.kernel
def bell(q0: Qubit, q1: Qubit):
    squin.h(q0)
    squin.cx(q0, q1)


main = squin.wrap(bell)

squin.wrap can infer the number of qubits from the wrapped kernel's unbound parameters, or accept an explicit n_qubits value when additional kernel parameters are bound by keyword:

@squin.kernel
def ansatz(q0: Qubit, q1: Qubit, theta: float):
    squin.rx(theta, q0)
    squin.cx(q0, q1)


main = squin.wrap(ansatz, 2, theta=0.125)

Changes

  • Added squin.wrap(method, n_qubits=None, **kwargs).
  • Exported wrap from bloqade.squin.
  • Added validation for non-kernel inputs, unexpected keyword arguments, invalid keyword names, negative qubit counts, and argument-count mismatches.
  • Added tests covering qubit-count inference, keyword binding, explicit qubit counts, and validation failures.

Notes

Kirin lowering does not currently support starred calls like kernel(*qubits), so squin.wrap generates a tiny wrapper function with explicit qubit indexing, for example kernel(qubits[0], qubits[1]), and then compiles that function through the normal squin.kernel path.

Testing

uv run --no-sync pytest test/squin/test_wrap.py test/squin/test_stdlib_shorthands.py
uv run --no-sync ruff check src/bloqade/squin/utils.py test/squin/test_wrap.py

@jasonhan3 jasonhan3 self-assigned this Apr 29, 2026
@jasonhan3 jasonhan3 added category: enhancement Category: this is an enhancement of an existing feature. area: stdlib Area: standard library related issues. labels Apr 29, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 29, 2026

Codecov Report

❌ Patch coverage is 92.59259% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/bloqade/squin/utils.py 92.45% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 29, 2026

☂️ Python Coverage

current status: ✅

Overall Coverage

Lines Covered Coverage Threshold Status
12295 10980 89% 0% 🟢

New Files

File Coverage Status
src/bloqade/squin/utils.py 92% 🟢
TOTAL 92% 🟢

Modified Files

File Coverage Status
src/bloqade/squin/_init_.py 100% 🟢
TOTAL 100% 🟢

updated for commit: f2ab24b by action🐍

@jasonhan3 jasonhan3 requested a review from david-pl April 29, 2026 21:11
Copy link
Copy Markdown
Collaborator

@david-pl david-pl left a comment

Choose a reason for hiding this comment

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

Generally, I think the feature idea in the issue is not well thought out.

Right now, the boilerplate you have to write to wrap a kernel is

@squin.kernel
def wrapped_kernel():
    q = squin.qalloc(10)
    kernel(q)
    return squin.broadcast.measure(q)

This is 4 lines, it doesn't get much simpler than that while keeping things generic. In the above, it's very simple to provide the exact arguments we need for each specific case. If we want to provide a utility function for this it either gets quite complicated or we have to make strong assumptions about the signature of the kernel we want to wrap, limiting the usefulness. The code here seems to be opting for the second approach. This is the better of the two IMO.

To implement this, we'd need to do something like this (pseudo code):

from kirin import ir
from kirin.dialects import func

body_block = ir.Block(
    stmts=[
        func.Invoke((n_qubits,), callee=squin.qalloc),  # allocate qubits
        func.Invoke((qubits,), callee=kernel),  # call the actual kernel
        func.Invoke((qubits,), callee=squin.broadcast.measure),  # measure
        func.Return(measurement_results)
    ]
)
body = ir.Region(blocks=[body_block])
code = func.Function(
    sym_name="wrapped",
    signature=...,
    body=ir.Region()
)
wrapped = ir.Method(
    dialects=kernel.dialects,
    code=code,
)

The current implementation is writing the kernel by composing strings, which is not the way to go here.

Also, keep in mind that the suggested implementation here only works for the specific kernel structure takes the list of qubits as the first argument. This is quite a specific assumption to have. Furthermore, the complexity added here is far from trivial. And all that, to save 4 lines of boiler plate. To be honest, I'd vote we simply do not implement this feature at all. We'll just have to live with the 4 lines of extra code.

From the issue, it also seems like the main use case is simulation. We might want to think about how to improve the simulator API to handle arguments more gracefully instead. The fundamental problem is, however, that you can only allocate qubits inside a kernel.

if call_args
else " __wrapped_method__()"
)
source = (
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Sorry, but no: we're not writing a wrapper kernel function by composing python strings. This is genuinely not a good idea.

@weinbe58
Copy link
Copy Markdown
Member

It should be possible to do this by just looking at the Method signature and then constructing a kernel that invokes the wrapped kernel, it would be the equivilant to the hard coded closure:

def wraps(mt: ir.Method, *args):
    code = mt.code
    if (trait := code.get_trait(HasSignature)) is None:
        raise ValueError("expecting a function that has a signature")
        
    signature = trait.get_signature(code)
    qubit_arg_map, other_arg_map, qubit_only_signature = analyze_signature(signature)
    
    wrapper_body = ir.Region([body_block := ir.Block()))
    # 1.  build block with the qubit arguments    
    # 2. insert logicl to add `py.Constant` for the constant args
    # 3. add invoke statement of `mt` with a mixture of the block arguments and the constant ssa values in the body
    # 4. construct the function statement and new method. 

cc: @jasonhan3 @david-pl

@github-actions
Copy link
Copy Markdown
Contributor

☂️ Code Coverage

current status: ✅

Overall Coverage

Statements Covered Coverage Threshold Status
12827 11477 89% 0% 🟢

New Files

File Coverage Status
src/bloqade/squin/utils.py 92% 🟢
TOTAL 92% 🟢

Modified Files

File Coverage Status
src/bloqade/squin/_init_.py 100% 🟢
TOTAL 100% 🟢

updated for commit: eb906f2 by action🐍

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces bloqade.squin.wrap, a helper that turns an existing squin.kernel (ir.Method) into a simulation-ready entry point by allocating n_qubits, invoking the kernel (with optional keyword bindings), measuring all allocated qubits, and returning measurement results. This fits into the squin layer as a convenience utility for running kernels without repeatedly writing allocation/measurement boilerplate.

Changes:

  • Added squin.wrap(method, n_qubits=None, **kwargs) with argument/keyword validation and wrapper codegen via a generated main() kernel.
  • Exported wrap from bloqade.squin.
  • Added a new test module covering inference, keyword binding, explicit qubit counts, and a few validation failures.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/bloqade/squin/utils.py Implements squin.wrap and its validation + wrapper compilation logic.
src/bloqade/squin/__init__.py Exposes wrap at the bloqade.squin package level.
test/squin/test_wrap.py Adds tests for core wrap behavior and several invalid-call cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +117 to +126
filename = (
f"<bloqade.squin.wrap:{wrapped_method.sym_name or 'anonymous'}:"
f"{next(_wrap_counter)}>"
)
linecache.cache[filename] = (
len(source),
None,
source.splitlines(keepends=True),
filename,
)
Comment on lines +34 to +45
if not isinstance(method, ir.Method):
raise TypeError(f"expected a Kirin Method, got {type(method).__name__}")

param_names = _method_param_names(method)
_validate_kwargs(param_names, kwargs)

if n_qubits is None:
n_qubits = len(param_names) - len(kwargs)

if n_qubits < 0:
raise ValueError("n_qubits must be non-negative")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: stdlib Area: standard library related issues. category: enhancement Category: this is an enhancement of an existing feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Utility function: "wrap" a kernel

5 participants