Skip to content
Merged
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
50 changes: 50 additions & 0 deletions swe-paddle/tasks/PaddlePaddle__Paddle-52948/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# PaddlePaddle__Paddle-52948

This directory converts Paddle PR #52948 and follow-up PR #53572 into one SWE-Paddle community task candidate (Hackathon No.91: `register_hook` for static / dy2static).

## Source

| Field | Value |
| --- | --- |
| Repo | `PaddlePaddle/Paddle` |
| Primary PR | [52948](https://github.com/PaddlePaddle/Paddle/pull/52948) |
| Follow-up PR | [53572](https://github.com/PaddlePaddle/Paddle/pull/53572) |
| PR titles | `【Hackathon No.91】` / `【Hackathon No.91】Following updates` |
| Base commit | `cf6cbc347970a1fd2c9d76e427880139789497af` |
| Gold endpoint | `f3f3d57a159caf3b77f93a4d86cb233e6a1c159a` (after #53572) |
| Merged at | `2023-04-27` (#52948), `2023-05-08` (#53572) |
| Task type | `feature_enhancement` |
| Resource | CPU (pure Python; era-matched Paddle / source checkout) |

## Summary

在静态图与动转静(`to_static`)场景下支持 `Tensor.register_hook`,使反向 hook
能正确触发,且梯度结果与动态图一致。完整 gold 覆盖首个合入实现及其后续规范化
接入(独立 Transformer 进入统一 AST 变换流水线、测试断言风格对齐)。

## Why This Sample

- **真实 Hackathon 闭环**:合入前相关单测显式断言 static / dy2static 下 hook 不可用。
- **框架级能力**:同时覆盖静态图 hook 运行时语义与动转静路径下的行为对齐。
- **双 PR 合一**:#52948 落地能力,#53572 按 review 意见完成 Transformer 规范化;合并后样本对应当前更合理的最终形态。
- **边界清晰**:目标集中在 hook 可运行且梯度一致;不要求内部函数 hook 与 `hook.remove`。

## Files

- `proposal.md`: approved proposal (maintainer triage context).
- `instruction.md`: self-contained problem statement for the coding agent.
- `solution/code.patch`: gold patch net of #52948 + #53572 (production files only).
- `tests/test.patch`: test patch exposing the target behavior.
- `tests/test.sh`: minimal target test command.
- `environment/README.md`: base commit, apply order, and reproduction notes.

## Verification

```bash
bash tests/test.sh
```

Expected behavior: with `tests/test.patch` applied on `base_commit`, the static /
dy2static `register_hook` cases should fail/error. After also applying
`solution/code.patch`, the target tests should pass. Existing dygraph
`register_hook` cases in the same module should remain pass-to-pass.
45 changes: 45 additions & 0 deletions swe-paddle/tasks/PaddlePaddle__Paddle-52948/environment/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Environment Notes

SWE-Paddle task candidate for PaddlePaddle/Paddle PR #52948 + follow-up #53572.

## Expected Environment

- Repository: `PaddlePaddle/Paddle`
- Base commit: `cf6cbc347970a1fd2c9d76e427880139789497af` (parent of #52948 squash-merge `db30aa1`)
- Gold endpoint: `f3f3d57a159caf3b77f93a4d86cb233e6a1c159a` (after #53572)
- Resource: CPU
- GPU required: no
- Patch type: **pure Python**. Production changes are limited to static-graph /
dy2static Python modules; no C++ / CUDA / kernel / infermeta rebuild is
required for the gold patch itself.
- Paddle install: prefer an era-matched (2023-04/05) wheel or a source checkout
at `base_commit` with a working Python package layout so that
`paddle.jit.to_static` and static-graph execution are available.

## Run Order (Run / Test / Fix)

1. Check out `PaddlePaddle/Paddle` at the base commit and ensure Paddle is
importable (era-matched wheel or source build/install).
2. Apply `tests/test.patch`.
3. Run `bash tests/test.sh`; the static / dy2static `register_hook` cases should
**fail / error** before the fix. Existing dygraph-only hook cases should still
pass (P2P candidates).
4. Apply `solution/code.patch`.
5. Run `bash tests/test.sh` again; all target cases should **pass**.

## Minimal Test Command

```bash
bash tests/test.sh
```

## Known Risks

- Historical (2023-04) wheels may be hard to pin exactly; if using a newer
wheel, confirm `to_static` / static Variable APIs still match the base-era
contracts exercised by the tests.
- `test_hook_in_init_for_layer` uses random input; prefer fixed seeds when
deriving stable F2P / P2P node IDs.
- Gold patch is the **net** of #52948 and #53572 relative to the base commit
(intermediate helpers introduced in #52948 and removed/relocated in #53572
are not left as dead intermediate state).
48 changes: 48 additions & 0 deletions swe-paddle/tasks/PaddlePaddle__Paddle-52948/instruction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 在静态图与动转静场景下支持 Tensor.register_hook

## 详细描述

当前动态图中可以对 Tensor 调用 `register_hook` 注册反向 hook,并在 `backward` 时按预期改写梯度。但在静态图,以及经 `paddle.jit.to_static`(或等价装饰器)转换后的路径上,该能力不可用或行为与动态图不一致。

需要达成的目标:

- 在静态图模式下,对可求导的 Tensor / Variable 调用 `register_hook` 应能成功执行,而不再因接口不可用而直接失败。
- 对经过 `to_static` 转换的函数或 `nn.Layer`,在中间结果或参数上注册的反向 hook 应能被触发。
- 同一计算在动态图与 `to_static` 路径下,hook 对梯度的影响应保持一致。至少覆盖以下场景:
- 多个中间变量分别注册 hook
- 变量被重新赋值后再注册 hook
- 同一变量重复注册 hook
- 在 `nn.Layer.__init__` 中对参数注册 hook,再对 `to_static` 后的网络做前向与反向
- 不要求支持:嵌套内部函数中的 `register_hook`,以及 `hook.remove`。

## 问题复现(示意)

在 `base_commit` 对应版本上:

1. 开启静态图,构造简单网络并对中间结果调用 `register_hook`,或对 `@to_static` / `to_static(...)` 包装后的函数/Layer 注册 hook 再 `backward`。
2. 可观察到接口不可用(例如断言失败),或 hook 未按动态图语义影响梯度。
3. 对照同一计算的纯动态图路径,梯度行为不一致或静态 / 动转静路径无法完成。

## 期望行为

- 静态图与动转静路径下 `register_hook` 可运行。
- hook 触发后的梯度结果与动态图一致(在上述覆盖场景内)。
- 既有动态图 `register_hook` 行为保持不变。

## 验收说明

- 上述目标行为可用,相关用例通过。
- 已有动态图 hook 语义不被破坏。
- 不允许通过删除测试、弱化断言或大范围绕过校验来「通过」任务。

## 技术要求

- 熟悉 Python
- 了解 Paddle 动态图 / 静态图与动转静(`to_static`)基本机制
- 了解 autograd hook 的基本语义

## Acceptance Criteria

- The behavior described above should be implemented.
- Existing valid behavior should remain unchanged.
- Do not satisfy the task by deleting tests, weakening assertions, or bypassing validation broadly.
146 changes: 146 additions & 0 deletions swe-paddle/tasks/PaddlePaddle__Paddle-52948/solution/code.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
diff --git a/python/paddle/fluid/framework.py b/python/paddle/fluid/framework.py
index 1111111..2222222 100644
--- a/python/paddle/fluid/framework.py
+++ b/python/paddle/fluid/framework.py
@@ -1640,9 +1640,24 @@
"""
pass

- @fake_interface_only
def register_hook(self, hook):
- pass
+ import paddle
+
+ def backward_hook_wrapper(dy):
+ """call the backward hook in ."""
+ return hook(np.array(dy))
+
+ def forward_hook_wrapper(x):
+ """do nothing but return a new variable."""
+ return x
+
+ paddle.static.py_func(
+ func=forward_hook_wrapper,
+ x=self,
+ out=self,
+ backward_func=backward_hook_wrapper,
+ skip_vars_in_backward_input=[self],
+ )

def __str__(self):
return self._to_readable_code()
diff --git a/python/paddle/jit/dy2static/ast_transformer.py b/python/paddle/jit/dy2static/ast_transformer.py
index 1111111..2222222 100644
--- a/python/paddle/jit/dy2static/ast_transformer.py
+++ b/python/paddle/jit/dy2static/ast_transformer.py
@@ -38,6 +38,7 @@
from .return_transformer import ReturnTransformer
from .static_analysis import StaticAnalysisVisitor
from .tensor_shape_transformer import TensorShapeTransformer
+from .tensorhook_transformer import RegisterHookTransformer
from .typehint_transformer import TypeHintTransformer
from .utils import ast_to_source_code

@@ -92,6 +93,7 @@
self.visit(node_wrapper.node)

transformers = [
+ RegisterHookTransformer,
EarlyReturnTransformer,
BasicApiTransformer, # Basic Api
TensorShapeTransformer, # Tensor.shape -> paddle.shape(Tensor)
diff --git a/python/paddle/jit/dy2static/tensorhook_transformer.py b/python/paddle/jit/dy2static/tensorhook_transformer.py
new file mode 100644
index 0000000..1111111
--- /dev/null
+++ b/python/paddle/jit/dy2static/tensorhook_transformer.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
+#
+# 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.
+
+import collections
+
+from paddle.utils import gast
+
+from .base_transformer import BaseTransformer
+
+
+class RegisterHookTransformer(BaseTransformer):
+ def __init__(self, wrapper_root):
+ self.register_hook_pos_map = collections.defaultdict(list)
+ self.assignment_pos_map = collections.defaultdict(list)
+ self.root = wrapper_root.node
+
+ def transform(self):
+ """
+ Main function to transform AST.
+ """
+ self.visit(self.root)
+
+ def visit_FunctionDef(self, func_def):
+ # The inner function that has register_hook will not be processed
+ check_register_hook = next(
+ (
+ node
+ for node in gast.walk(func_def)
+ if isinstance(node, gast.Attribute)
+ and node.attr == 'register_hook'
+ ),
+ None,
+ )
+ if check_register_hook is None:
+ return func_def
+
+ register_hook_pos_map = self.register_hook_pos_map
+ assignment_pos_map = self.assignment_pos_map
+
+ for i in range(len(func_def.body) - 1, -1, -1):
+
+ body = func_def.body[i]
+ # Check if the code body contains the register_hook
+ if isinstance(body, gast.Expr):
+ for node in gast.walk(body):
+ if (
+ isinstance(node, gast.Attribute)
+ and node.attr == 'register_hook'
+ ):
+ # parameter name for register_hook
+ param_name = node.value.id
+ register_hook_pos_map[param_name].append(i)
+ elif isinstance(body, gast.Assign):
+ for target in body.targets:
+ assignment_pos_map[target.id].append(i)
+
+ # Confirm the order
+ order_map = {}
+ for k, idx_list in register_hook_pos_map.items():
+ for idx in idx_list:
+ if k not in assignment_pos_map:
+ order_map[idx] = 1
+ else:
+ for assignment_idx in assignment_pos_map[k]:
+ if idx > assignment_idx:
+ order_map[idx] = assignment_idx + 1
+ break
+ code_order = [*range(len(func_def.body))]
+ for k, v in sorted(order_map.items(), key=lambda x: x[1], reverse=True):
+ if k == v:
+ continue
+ code_order.remove(k)
+ code_order.insert(v, k)
+
+ # rearrange the code according to the specified order
+ new_body = [func_def.body[i] for i in code_order]
+ func_def.body = new_body
+ return func_def
Loading