Skip to content
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

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

Jaybsoni
Copy link
Contributor

@Jaybsoni Jaybsoni commented Apr 30, 2024

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:

  • Added a PLDB class which inherits most of its functionality from Pdb.
  • Added functionality to pass active device information into the debugger during qnode execution to validate the compatibility between the device and the debugger.

image

@Jaybsoni Jaybsoni added the WIP 🚧 Work-in-progress label Apr 30, 2024
Copy link
Contributor

Hello. You may have forgotten to update the changelog!
Please edit doc/releases/changelog-dev.md 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.

@Jaybsoni
Copy link
Contributor Author

[sc-62243]

@Jaybsoni
Copy link
Contributor Author

Open Questions:

  • Do we want to take any specialized actions when users call step down or step up? Restrict it? Raise a warning? (Currently it is allowed and leads the user into complex pennylane internals)
  • How should we test the breakpoint functionality without triggering the interactive debugger in the test suite? Are there any industry standards here?
  • I have currently added validation checks for incompatible devices (anything other than lightning.qubit and default.qubit) as well is if the breakpoint is called outside of a qnode execution. Any thoughts on this?

@Jaybsoni Jaybsoni changed the title [WIP] Implement qml.breakpoint() and initial integration with Pdb Implement qml.breakpoint() and initial integration with Pdb Apr 30, 2024
@Jaybsoni Jaybsoni added review-ready 👌 PRs which are ready for review by someone from the core team. and removed WIP 🚧 Work-in-progress labels Apr 30, 2024
Args:
dev (Union[Device, "qml.devices.Device"]): The active device
"""
cls.__active_dev.append(dev)
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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,

pennylane/debugging.py Show resolved Hide resolved
Comment on lines +922 to +925
if PLDB.is_active_dev():
PLDB.reset_active_dev()

PLDB.add_device(self.device)
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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.

Copy link
Contributor

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.

Copy link

codecov bot commented Apr 30, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 99.67%. Comparing base (fbc2a39) to head (26f28f9).
Report is 1 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

@trbromley trbromley left a comment

Choose a reason for hiding this comment

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

Thanks @Jaybsoni!

Comment on lines +113 to +114
class PLDB(pdb.Pdb):
"""Custom debugging class integrated with Pdb."""
Copy link
Contributor

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?

Copy link
Contributor Author

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."""
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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

@Jaybsoni Jaybsoni requested review from mlxd and trbromley May 10, 2024 21:02
@Jaybsoni Jaybsoni requested a review from Mandrenkov May 23, 2024 20:15
Copy link
Contributor

@DSGuala DSGuala left a 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
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe:

Suggested change
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")
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
raise TypeError("Device not supported with breakpoint")
raise TypeError("Breakpoints not supported on this device")

Comment on lines +922 to +925
if PLDB.is_active_dev():
PLDB.reset_active_dev()

PLDB.add_device(self.device)
Copy link
Contributor

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.

Copy link
Contributor

@Mandrenkov Mandrenkov left a 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):
Copy link
Contributor

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. 😆

Copy link
Contributor Author

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

Copy link
Contributor

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"):
Copy link
Contributor

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")
Copy link
Contributor

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")
Copy link
Contributor

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:

Suggested change
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)
Copy link
Contributor

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)
Copy link
Contributor

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.

Comment on lines +929 to +935
# 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)
Copy link
Contributor

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.

tests/test_debugging.py Show resolved Hide resolved

PLDB.reset_active_dev() # clean up the debugger active devices

dev_names = (
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

@trbromley trbromley left a 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):
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
review-ready 👌 PRs which are ready for review by someone from the core team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants