-
Notifications
You must be signed in to change notification settings - Fork 547
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement qml.breakpoint()
and initial integration with Pdb
#5600
base: master
Are you sure you want to change the base?
Conversation
Hello. You may have forgotten to update the changelog!
|
[sc-62243] |
Open Questions:
|
qml.breakpoint()
and initial integration with Pdb
qml.breakpoint()
and initial integration with Pdb
Args: | ||
dev (Union[Device, "qml.devices.Device"]): The active device | ||
""" | ||
cls.__active_dev.append(dev) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So devices are add-only, not not selectively removed --- as in, we expect them to be appended, and reset the entire list to remove?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes exactly! In fact, instead of appending, we could probably just override the 1st entry in the list each time, since there shouldn't be more than one "active device" at a time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, if only 1 device is a constraint, adapting to match that design sounds reasonable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we only need to support one device, it could be worth making the active device just a ClassVar
. It's minimal syntax and simple to use. If, however, we think in the future it is likely that we'll need to support multiple devices, then defining these methods makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we will want multi-device support in the future,
if PLDB.is_active_dev(): | ||
PLDB.reset_active_dev() | ||
|
||
PLDB.add_device(self.device) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a quick check, but this setup shouldn't have any problem with concurrent execution, right? If we in parallel create 2 qnodes with 2 devices, what happens here if they try to modify the class at the same time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They will most definitely clash! I definitely think this feature should not be used with concurrent execution, I don't know if a user would ever want to actually debug the circuit for 2 qnodes concurrently?
I based this implementation on how the Queuing manger is structured, how does that correspond with concurrent execution
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to implement this without a queuing manager approach?
If not, as a quick validation, can we try using the debugger with default.qubit
and max_workers >= 2 to see what happens.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried this with max_workers=3
and it seemed to work normally (had to reduce OMP_NUM_THREADS). Not sure if it will still work for qml.debugging.state()
when it is implemented.
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #5600 +/- ##
==========================================
- Coverage 99.68% 99.67% -0.01%
==========================================
Files 416 416
Lines 39105 38432 -673
==========================================
- Hits 38981 38307 -674
- Misses 124 125 +1 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @Jaybsoni!
class PLDB(pdb.Pdb): | ||
"""Custom debugging class integrated with Pdb.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To confirm, this isn't something users would typically interact with?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes exactly! We don't want the user calling the PLDB class at all. In the same way we wouldn't want them to interact with the QueuingManager
class for example.
|
||
|
||
def breakpoint(): | ||
"""Launch the custom PennyLane debugger.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the plan with docs here - normally PRs should have detailed docs, especially for major user-facing functions like this. Though if we have a story to track a subsequent docs revisit and polish, I'm fine with that too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a story to revisit the docs and polish. I will add a little more detail to the breakpoint docs in this PR, but once all of the functionality (measuring states, queuing gates in debug mode, etc.) are added in the subsequent PRs, then we can finalize its docstring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor point that we can discuss in a docs PR:
We don't have a quickstart (.rst) for debugging
yet. And it doesn't seem like we make quickstarts for any of the high level files in the pennylane
folder. Maybe there's a question here about whether debugging functionality should go in pennylane/debugging.py
or in pennylane/debugging/debugging.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
qml.breakpoint()
pauses execution ✔️- Tested the commands while debugging (list, next, continue, quit) and they all worked fine. ✔️ One note here: hitting next at the return statement of the circuit puts us into PL internals, but I think that's fine.
- Can access internal and external variables from the debugging context ✔️
- Can queue additional operations ✔️
Pending for next PRs:
- qml.debugging.state()
- qml.debugging.expval()
- qml.debugging.tape()
I think we can keep this open and merge other PRs into this branch. Having the docs will especially help with PR reviews.
|
||
Raises: | ||
TypeError: Can't call breakpoint outside of a qnode execution | ||
TypeError: Device not supported with breakpoint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe:
TypeError: Device not supported with breakpoint | |
TypeError: Breakpoints not supported on this device |
raise TypeError("Can't call breakpoint outside of a qnode execution") | ||
|
||
if cls.get_active_device().name not in ("default.qubit", "lightning.qubit"): | ||
raise TypeError("Device not supported with breakpoint") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
raise TypeError("Device not supported with breakpoint") | |
raise TypeError("Breakpoints not supported on this device") |
if PLDB.is_active_dev(): | ||
PLDB.reset_active_dev() | ||
|
||
PLDB.add_device(self.device) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried this with max_workers=3
and it seemed to work normally (had to reduce OMP_NUM_THREADS). Not sure if it will still work for qml.debugging.state()
when it is implemented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat stuff, @Jaybsoni! I left a few minor comments and suggestions.
@@ -105,3 +108,75 @@ def get_snapshots(*args, **kwargs): | |||
return dbg.snapshots | |||
|
|||
return get_snapshots | |||
|
|||
|
|||
class PLDB(pdb.Pdb): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Satire) Missed opportunity to call this Qdb
. 😆
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could still change it! Thoughts @trbromley
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I defer to @DSGuala, but either way works for me! I don't think users will interact with this functionality so would be happy to stick with PLDB
.
if not qml.queuing.QueuingManager.recording() or not cls.is_active_dev(): | ||
raise TypeError("Can't call breakpoint outside of a qnode execution") | ||
|
||
if cls.get_active_device().name not in ("default.qubit", "lightning.qubit"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question
Is this because there is something specific about the implementation of these devices? Or just because they have a certain interface? If it's the latter, we should define the interface using a Protocol
decorated with runtime_checkable()
and simply check if the device supports the interface here.
""" | ||
|
||
if not qml.queuing.QueuingManager.recording() or not cls.is_active_dev(): | ||
raise TypeError("Can't call breakpoint outside of a qnode execution") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick
Is this really a type error? Or is it more of a RuntimeError
?
Union[Device, "qml.devices.Device"]: The active device | ||
""" | ||
if not cls.is_active_dev(): | ||
raise ValueError("No active device to get") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick
Similarly, the issue is not that an argument to this method is incorrect:
raise ValueError("No active device to get") | |
raise RuntimeError("No active device to get") |
Args: | ||
dev (Union[Device, "qml.devices.Device"]): The active device | ||
""" | ||
cls.__active_dev.append(dev) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we only need to support one device, it could be worth making the active device just a ClassVar
. It's minimal syntax and simple to use. If, however, we think in the future it is likely that we'll need to support multiple devices, then defining these methods makes sense.
|
||
def __init__(self, *args, **kwargs): | ||
"""Initialize the debugger, and set custom prompt string.""" | ||
super().__init__(*args, **kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note
We can pass a skip
argument to avoid stepping into PennyLane internals. From the docs,
The skip argument, if given, must be an iterable of glob-style module name patterns. The debugger will not step into frames that originate in a module that matches one of these patterns.
# Before constructing the tape, we pass the device to the | ||
# debugger to ensure they are compatible if there are any | ||
# breakpoints in the circuit | ||
if PLDB.is_active_dev(): | ||
PLDB.reset_active_dev() | ||
|
||
PLDB.add_device(self.device) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion
This works, although it could potentially make sense to just have the "setting" of an active device as a context manager (so that the caller doesn't need to "remember" to reset the active device). This is similar to the approach JAX uses.
|
||
PLDB.reset_active_dev() # clean up the debugger active devices | ||
|
||
dev_names = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What prevents us from supporting other devices?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As long as its a simulator device, we should be able to support it, but for this MVP we are aimining to gurantee support for these two devices.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No blockers to merging from me! Thanks @Jaybsoni!
@@ -105,3 +108,75 @@ def get_snapshots(*args, **kwargs): | |||
return dbg.snapshots | |||
|
|||
return get_snapshots | |||
|
|||
|
|||
class PLDB(pdb.Pdb): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I defer to @DSGuala, but either way works for me! I don't think users will interact with this functionality so would be happy to stick with PLDB
.
Context:
Part of the effort to add more tools for algorithmic debugging of quantum circuits with PennyLane. In this PR we introduce the
qml.breakpoint()
function which allows users to insert breakpoints into their quantum workflow and enter a debugging context (via Pdb) to step through their quantum functions.Description of the Change: