-
Notifications
You must be signed in to change notification settings - Fork 25
[Graph] Add qd.checkpoint #725
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
base: main
Are you sure you want to change the base?
Changes from 42 commits
9c3920d
ec5bdd2
1af49f0
1e18653
a5c614f
b4612a0
e138cfd
4492b35
79ebf57
28a2f29
5dedc4e
977f3f0
bf906d7
dd37b28
788f607
37ed4e4
4d5bc25
d7fe3f6
7123030
c685f24
0c94986
e35e9cb
83a6224
b265cdf
e42c496
9a369c4
22f8d26
22f2069
a25c241
840ac3d
68a4d54
cd2881d
fcc001f
559fbbf
fded02a
73628e2
01795ab
0983393
5058e34
c8bfb4b
7c9b76a
61ee679
a0f2b54
cf4633e
b6ca290
d2afddf
b6ee9be
2a483e7
9240ca3
fb799c6
6ea701b
8b45f47
dd9548b
d741a46
197a892
3402601
9e3eae5
10c36a6
5b3e656
0e26368
9aa7c69
0bb6027
935cc37
7ddee8d
5d1c181
7aa1f60
b3e726e
9698f73
60290d7
aa408c6
9e02885
a7f198f
9d91543
24139e0
be27aa3
2db5db8
9a35cb9
b0ae987
c5fa6c9
9e2cc90
a3520a0
14edbf7
5927d31
d3559df
f21617d
67da464
a398139
e939301
6544ff6
0464423
cb4f308
28f630b
1cf0343
35e49d4
3b84936
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,14 +4,11 @@ Graphs reduce kernel launch overhead by capturing a sequence of GPU operations i | |
|
|
||
| ## Backend support | ||
|
|
||
| Both features run on every backend. They are *hardware accelerated* on CUDA (via CUDA graphs) and AMDGPU (via HIP graphs); `graph_do_while` additionally requires CUDA SM 9.0+ / Hopper for its hardware-accelerated path. On other backends, `graph=True` is silently ignored and the kernel runs via the normal launch path, and `graph_do_while` falls back to a host-side do-while loop that copies the condition value GPU → host each iteration (causing a pipeline stall — see [Caveats](#caveats)). | ||
|
|
||
| | Feature | `qd.cuda` SM 9.0+ | `qd.cuda` < SM 9.0 | `qd.amdgpu` | `qd.metal` | `qd.vulkan` | `qd.cpu` | | ||
|
hughperkins marked this conversation as resolved.
|
||
| | --- | --- | --- | --- | --- | --- | --- | | ||
| | `graph=True` | hardware accelerated | hardware accelerated | hardware accelerated | runs (no acceleration) | runs (no acceleration) | runs (no acceleration) | | ||
| | `graph_do_while` | hardware accelerated | host fallback | host fallback | host fallback | host fallback | host fallback | | ||
|
|
||
| AMDGPU `graph_do_while` falls back to the host-side loop because HIP does not currently expose conditional / while graph nodes (as of ROCm 7.2). | ||
| | `qd.checkpoint` (skip + `yield_on=`) | GPU-side | GPU-side | GPU-side | GPU-side | GPU-side | host-side | | ||
|
hughperkins marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Basic usage | ||
|
|
||
|
|
@@ -154,3 +151,65 @@ Note: the basic `graph=True` path (without `graph_do_while`) does **not** stall | |
| Therefore on unsupported platforms, you might consider creating a second implementation, which works differently. e.g.: | ||
| - fixed number of loop iterations, so no dependency on gpu data for kernel launch; combined perhaps with: | ||
| - make each kernel 'short-circuit', exit quickly, if the task has already been completed; to avoid running the GPU more than necessary | ||
|
|
||
| ## Checkpoints with `qd.checkpoint` *(experimental)* | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: after the backwards-incompatible disaster taht was the algorithms.md changes for qipc, that kept changing 😅 I think I'd like to mark things as 'experimental' for a few weeks/months, until we are confident the api is stable.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note to AI: this is not a request to you, it is an obseration for other human reviewers.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If checkpoint is a graph-specific API, it should be prefixed by 'graph_', as for all the other functions.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Know that I am thinking, we should probably just have some new 'qd.graph.' submodule. That would like everything both simpler and more less confusing.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems reasonable. But perhaps not in this PR?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case, make sure it is tracking somewhere. Still, it is weird to address this in another PR, since it is literally the PR that introduces this function. But I can understand you want to move faster and qipc is already relying on this one. |
||
|
|
||
| `qd.checkpoint()` marks a section of a graph kernel as a *skippable, optionally yieldable stage*. An example use-case is an algorithm implemented as a graph where you might need to allocate additional memory part-way through, the graph operations are in-place, and simply retrying the whole graph from the start is not an option. `qd.checkpoint` lets the kernel break at some point in the graph, surface the reason to the host, let the host fix things up, and resume from that point on the next launch. | ||
|
hughperkins marked this conversation as resolved.
Outdated
hughperkins marked this conversation as resolved.
Outdated
hughperkins marked this conversation as resolved.
Outdated
|
||
|
|
||
| ```python | ||
| @qd.kernel(graph=True) | ||
| def step( | ||
| arr: qd.types.ndarray(qd.f32, ndim=1), | ||
| overflow_flag: qd.types.ndarray(qd.i32, ndim=0), | ||
| newton_cond: qd.types.ndarray(qd.i32, ndim=0), | ||
| ): | ||
| while qd.graph_do_while(newton_cond): | ||
| with qd.checkpoint(): # cp_id 0 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice to be able to specify some enum-based ID, to allow cleaner implementation of checkpoint recovery.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean, instead of incremntally auto-assigning an integer id, require the id to be passed in, as an integer, and we could pass in an intenum value, converted to int?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes exactly. |
||
| for i in range(arr.shape[0]): | ||
| # ... | ||
| pass | ||
| with qd.checkpoint(yield_on=overflow_flag): # cp_id 1 (can yield) | ||
| for i in range(arr.shape[0]): | ||
| # ... | ||
| pass | ||
| with qd.checkpoint(): # cp_id 2 | ||
| for i in range(arr.shape[0]): | ||
| # ... | ||
| pass | ||
|
hughperkins marked this conversation as resolved.
Outdated
|
||
| ``` | ||
|
hughperkins marked this conversation as resolved.
|
||
|
|
||
| Each `with qd.checkpoint(...)` block gets a `cp_id` assigned. You can use the `cp_id` to identify which checkpoint yielded and which checkpoint to resume from — see [Host-side yield / resume loop](#host-side-yield--resume-loop) below. | ||
|
|
||
| ### Yield mechanism | ||
|
|
||
| If `yield_on=foo` is supplied, the body may write a non-zero value into `foo[()]` (for example, when a pre-allocated buffer is too small) to signal "the host needs to handle something before this checkpoint can complete". When that happens: | ||
|
duburcqa marked this conversation as resolved.
Outdated
|
||
|
|
||
| 1. The framework records the checkpoint that yielded (first yielder in declaration order wins). | ||
| 2. Every later checkpoint in the same launch is skipped. | ||
| 3. `qd.checkpoint` will exit any surrounding `qd.graph_do_while`. | ||
| 4. `foo[()]` is reset to `0`. | ||
|
hughperkins marked this conversation as resolved.
Outdated
|
||
|
|
||
| ### Host-side yield / resume loop | ||
|
|
||
| Kernels with at least one `yield_on=` checkpoint return a `qd.GraphStatus` from every launch (and from `kernel.resume(...)`). The status carries two fields: | ||
|
|
||
| - `status.yielded` — `True` iff some `yield_on=` flag was non-zero during this launch. | ||
| - `status.checkpoint` — `cp_id` of the first (in declaration order) checkpoint that fired its flag, or `None` when `yielded` is `False`. | ||
|
|
||
| Resume by calling `kernel.resume(..., from_checkpoint=status.checkpoint)`. Every `qd.checkpoint` with `cp_id < from_checkpoint` is skipped on the resume launch; the rest run normally. The canonical host loop: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Does this means the checkpoint for 'cp_id == from_checkpoint' will be executed once again entirely?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be clearly stated if not already the case. |
||
|
|
||
| ```python | ||
| status = step(arr, overflow_flag, newton_cond) | ||
|
duburcqa marked this conversation as resolved.
|
||
| while status.yielded: | ||
| handle_overflow_for(status.checkpoint, ...) | ||
| status = step.resume(arr, overflow_flag, newton_cond, | ||
| from_checkpoint=status.checkpoint) | ||
| ``` | ||
|
|
||
| Kernels with `qd.checkpoint()` but no `yield_on=` keep their previous return contract (typically `None`) — the `GraphStatus` surface is opt-in via `yield_on=`. | ||
|
hughperkins marked this conversation as resolved.
Outdated
|
||
|
|
||
| ### Restrictions | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you explain / be more explicit about what happens during resume? Stating clearly that the entire checkpoint block is re-executed, and that it is user-responsibility to ensure idempotent behaviour when checkpoint is needed? Because if the state is altered during the checkpoint block, resuming is not going to save you I guess? Whatever the answer, it should be very clear in the doc. Beyond that, how does checkpointing works under the hood? Does it snapshot all the input data by copy before yielding, or it just return like this? If no copy is made, this means that resuming must be done "right away", without further altering the data in between, otherwise it is some kind of undefined behaviour. Another important point, what is I don't want to resume in such a case and I just want to move on to another kernel and continue like this? Is it supported or resume must happen? I think it is essentially to clarify all these points in the documentation.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. checkpoint does NOT require idempotent behavior. This is the entire purpose of checkpoint: to be able to interrupt and resume graphs that are NOT idempotent.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah, hte checkpoint block itself. right.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the checkpoint block itself actually does not so much require idempotence, as requiring that it is atomic: it either succeeds completely, or fails without changing anything.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as an example, in the case of allocation issues, the checkpoint block looks like:
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added 'resume where' section
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, this is exactly what I meant by « ensure idempotent behaviour when checkpoint is needed »
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "fails without changing anything" I feel is not idempotent? Idempotent means that calling the function multiple times is identical in effect to calling it once. But if it fails the first time, it would only be idempotent if it always failed thereafter I feel? |
||
|
|
||
| - Must be used inside `@qd.kernel(graph=True)`. | ||
| - `yield_on=` (when supplied) must be a kernel parameter that is a 0-d `qd.types.ndarray(qd.i32, ndim=0)`. | ||
| - Checkpoints cannot be nested inside other checkpoints. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1362,6 +1362,42 @@ def _is_graph_do_while_call(node: ast.expr) -> str | None: | |
| return node.args[0].id | ||
| return None | ||
|
|
||
| @staticmethod | ||
| def _is_checkpoint_call(node: ast.expr) -> tuple[bool, str | None]: | ||
| """If *node* is a ``qd.checkpoint(...)`` call return ``(True, yield_on_arg_name)``; otherwise | ||
| ``(False, None)``. ``yield_on_arg_name`` is ``None`` when the user wrote | ||
| ``qd.checkpoint()`` with no ``yield_on`` kwarg. | ||
|
|
||
| Validates the call shape (no positional args, only ``yield_on=`` as a bare ``ast.Name``) | ||
| and raises ``QuadrantsSyntaxError`` for misuse so the user gets a clear message at the | ||
| ``with`` site rather than a vague "not stream_parallel" error later. | ||
| """ | ||
| if not isinstance(node, ast.Call): | ||
| return False, None | ||
| func = node.func | ||
| is_checkpoint = (isinstance(func, ast.Attribute) and func.attr == "checkpoint") or ( | ||
| isinstance(func, ast.Name) and func.id == "checkpoint" | ||
| ) | ||
| if not is_checkpoint: | ||
| return False, None | ||
| if node.args: | ||
| raise QuadrantsSyntaxError( | ||
| "qd.checkpoint() takes no positional arguments; use qd.checkpoint(yield_on=flag) instead" | ||
| ) | ||
| yield_on_name: str | None = None | ||
| for kw in node.keywords: | ||
| if kw.arg != "yield_on": | ||
| raise QuadrantsSyntaxError( | ||
| f"qd.checkpoint() got unexpected keyword argument {kw.arg!r}; only 'yield_on' is supported" | ||
| ) | ||
| if not isinstance(kw.value, ast.Name): | ||
| raise QuadrantsSyntaxError( | ||
| "qd.checkpoint(yield_on=...) must be the bare name of a kernel parameter " | ||
| "(e.g. `yield_on=overflow_flag`); expressions are not supported" | ||
| ) | ||
| yield_on_name = kw.value.id | ||
| return True, yield_on_name | ||
|
|
||
| @staticmethod | ||
| def build_While(ctx: ASTTransformerFuncContext, node: ast.While) -> None: | ||
| if node.orelse: | ||
|
|
@@ -1575,15 +1611,112 @@ def build_With(ctx: ASTTransformerFuncContext, node: ast.With) -> None: | |
| raise QuadrantsSyntaxError("'with ... as ...' is not supported in Quadrants kernels") | ||
| if not isinstance(item.context_expr, ast.Call): | ||
| raise QuadrantsSyntaxError("'with' in Quadrants kernels requires a call expression") | ||
|
|
||
| is_checkpoint, yield_on_name = ASTTransformer._is_checkpoint_call(item.context_expr) | ||
| if is_checkpoint: | ||
| return ASTTransformer._build_checkpoint_with(ctx, node, yield_on_name) | ||
|
|
||
| if not FunctionDefTransformer._is_stream_parallel_with(node, ctx.global_vars): | ||
| raise QuadrantsSyntaxError("'with' in Quadrants kernels only supports qd.stream_parallel()") | ||
| raise QuadrantsSyntaxError( | ||
| "'with' in Quadrants kernels only supports qd.stream_parallel() or qd.checkpoint()" | ||
| ) | ||
| if not ctx.is_kernel: | ||
| raise QuadrantsSyntaxError("qd.stream_parallel() can only be used inside @qd.kernel, not @qd.func") | ||
| ctx.ast_builder.begin_stream_parallel() | ||
| build_stmts(ctx, node.body) | ||
| ctx.ast_builder.end_stream_parallel() | ||
| return None | ||
|
|
||
| @staticmethod | ||
| def _build_checkpoint_with( | ||
| ctx: ASTTransformerFuncContext, | ||
| node: ast.With, | ||
| yield_on_name: str | None, | ||
| ) -> None: | ||
| """Handles ``with qd.checkpoint(yield_on=arg):`` blocks. | ||
|
|
||
| Slice 1a: validates the use-site (kernel must be graph=True, no nesting, yield_on must be a kernel | ||
| parameter) and records the checkpoint's ``yield_on`` arg on the kernel object. Walks the body | ||
| transparently -- for-loops inside the ``with`` become normal top-level for-loops in the kernel's | ||
| frontend IR. The ``cp_id`` is assigned by declaration order (list index in | ||
| ``kernel.checkpoint_yield_on_args``). | ||
|
|
||
| Later slices wire ``cp_id`` through ForLoopConfig → OffloadedTask so the GraphManager can wrap | ||
| each checkpoint's body kernels in an IF conditional node and insert the yield-check kernel. | ||
| """ | ||
| if not ctx.is_kernel: | ||
| raise QuadrantsSyntaxError("qd.checkpoint() can only be used inside @qd.kernel, not @qd.func") | ||
| kernel = ctx.global_context.current_kernel | ||
| if not kernel.use_graph: | ||
| raise QuadrantsSyntaxError("qd.checkpoint() requires @qd.kernel(graph=True)") | ||
| if getattr(ctx, "_in_checkpoint", False): | ||
| raise QuadrantsSyntaxError( | ||
| "qd.checkpoint() cannot be nested inside another qd.checkpoint(); checkpoints in the " | ||
| "same kernel must be flat siblings (a checkpoint inside qd.graph_do_while is fine)" | ||
| ) | ||
| if yield_on_name is not None: | ||
| arg_names = [m.name for m in kernel.arg_metas] | ||
| if yield_on_name not in arg_names: | ||
| raise QuadrantsSyntaxError( | ||
| f"qd.checkpoint(yield_on={yield_on_name!r}) does not match any parameter of kernel " | ||
| f"{kernel.func.__name__!r}. Available parameters: {arg_names}" | ||
| ) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This only checks that Useful? React with 👍 / 👎. |
||
|
|
||
| # Auto-wrap bare top-level statements in the checkpoint body in a one-iteration | ||
| # `for` loop. The offloader's pending-serial bucket loses the surrounding | ||
| # `checkpoint_id` and emits such statements as `serial` tasks with `cp_id == -1`, | ||
| # meaning they would run unconditionally even when the checkpoint is skipped -- a | ||
| # silent correctness bug. The fix is to lower them as `range_for` tasks instead by | ||
| # wrapping each bare statement in `for _ in range(1): <stmt>`. We target the specific | ||
| # statement kinds known to hit the footgun (Assign / AugAssign / AnnAssign / | ||
| # non-docstring Expr) and leave everything else (For, While, If, With, Pass, | ||
| # docstring) untouched so they keep working transparently; nested | ||
| # `with qd.checkpoint(...)` in particular still falls through to the existing | ||
| # nested-checkpoint check at the start of this method. | ||
| new_body: list[ast.stmt] = [] | ||
| for i, stmt in enumerate(node.body): | ||
| needs_wrap = isinstance(stmt, (ast.Assign, ast.AugAssign, ast.AnnAssign)) | ||
| if not needs_wrap and isinstance(stmt, ast.Expr): | ||
| is_docstring = i == 0 and isinstance(stmt.value, ast.Constant) | ||
| needs_wrap = not is_docstring | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a checkpoint body starts with a top-level Useful? React with 👍 / 👎. |
||
| if needs_wrap: | ||
| wrapped = ast.For( | ||
| target=ast.Name(id="_", ctx=ast.Store()), | ||
| iter=ast.Call( | ||
| func=ast.Name(id="range", ctx=ast.Load()), | ||
| args=[ast.Constant(value=1)], | ||
| keywords=[], | ||
| ), | ||
| body=[stmt], | ||
| orelse=[], | ||
| ) | ||
| ast.copy_location(wrapped, stmt) | ||
| ast.fix_missing_locations(wrapped) | ||
| new_body.append(wrapped) | ||
| else: | ||
| new_body.append(stmt) | ||
| node.body = new_body | ||
|
|
||
| kernel.checkpoint_yield_on_args.append(yield_on_name) | ||
| # Hand control to the C++ ASTBuilder so that every for-loop emitted by `build_stmts` | ||
| # below is tagged with this checkpoint's `cp_id` on its `ForLoopConfig.checkpoint_id`. | ||
| # The C++ counter is the source of truth for cp_id; we cross-check it against the | ||
| # Python list index so a future refactor that misaligns the two surfaces immediately. | ||
| cpp_cp_id = ctx.ast_builder.begin_checkpoint() | ||
| py_cp_id = len(kernel.checkpoint_yield_on_args) - 1 | ||
| assert cpp_cp_id == py_cp_id, ( | ||
| f"C++ ASTBuilder.begin_checkpoint() returned cp_id={cpp_cp_id} but Python " | ||
| f"kernel.checkpoint_yield_on_args index expected {py_cp_id}; these counters " | ||
| f"must stay in lockstep so the GraphManager (slice 1c) can index yield_on by cp_id" | ||
| ) | ||
| ctx._in_checkpoint = True | ||
| try: | ||
| build_stmts(ctx, node.body) | ||
| finally: | ||
| ctx._in_checkpoint = False | ||
| ctx.ast_builder.end_checkpoint() | ||
| return None | ||
|
|
||
| @staticmethod | ||
| def build_Pass(ctx: ASTTransformerFuncContext, node: ast.Pass) -> None: | ||
| return None | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| """Plain-Python container returned from graph kernels that contain ``qd.checkpoint(yield_on=...)``. | ||
|
|
||
| Lives in its own module (with no Quadrants-internal imports) so it can be imported safely from | ||
| both ``kernel.py`` and ``misc.py`` without re-introducing the circular import chain that | ||
| ``misc.py -> impl.py -> kernel.py`` would create. | ||
|
|
||
| Re-exported via ``qd.lang.misc`` (and therefore as ``qd.GraphStatus``) for the user-facing | ||
| canonical import path. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
|
|
||
| class GraphStatus: | ||
| """Result returned by a graph kernel that contains ``qd.checkpoint(yield_on=...)`` blocks. | ||
|
|
||
| Returned from ``kernel(...)`` and ``kernel.resume(..., from_checkpoint=cp)`` whenever the | ||
| kernel was decorated with ``@qd.kernel(graph=True)`` and contains at least one checkpoint | ||
| that declared a ``yield_on=`` parameter. Read ``status.yielded`` to decide whether to keep | ||
| running the host loop, and ``status.checkpoint`` to find out which checkpoint asked the | ||
| host to handle something. | ||
|
|
||
| Canonical usage (mirrors the qipc re-entrant pattern; see ``graph.md``):: | ||
|
|
||
| status = step(arr, overflow_flag, newton_cond) | ||
| while status.yielded: | ||
| handle_overflow_for(status.checkpoint, ...) | ||
| status = step.resume(arr, overflow_flag, newton_cond, | ||
| from_checkpoint=status.checkpoint) | ||
|
|
||
| Attributes: | ||
| yielded: ``True`` iff one of the kernel's ``yield_on=`` checkpoints fired its flag on | ||
| the most recent launch. ``False`` means the kernel completed normally and the host | ||
| loop should exit. | ||
| checkpoint: ``cp_id`` of the checkpoint whose ``yield_on=`` flag was non-zero (or | ||
| ``None`` when ``yielded`` is ``False``). Pass it to ``kernel.resume(..., | ||
| from_checkpoint=cp)`` to skip every checkpoint with a lower ``cp_id`` on the next | ||
| launch. | ||
| """ | ||
|
|
||
| __slots__ = ("yielded", "checkpoint") | ||
|
|
||
| def __init__(self, yielded: bool, checkpoint: int | None): | ||
| self.yielded = yielded | ||
| self.checkpoint = checkpoint | ||
|
|
||
| def __repr__(self) -> str: | ||
| if self.yielded: | ||
| return f"GraphStatus(yielded=True, checkpoint={self.checkpoint})" | ||
| return "GraphStatus(yielded=False)" |
Uh oh!
There was an error while loading. Please reload this page.