已合并
[fix] support memory reuse in launch_host_func_pending #36054
kita-ikuyo创建于 5月19日
[fix] support memory reuse in launch_host_func_pending #36054
已合并
kita-ikuyo创建于 5月19日
4 个文件变更+375-72
@@ -1,14 +1,15 @@
1import os1import os
2import tempfile2import tempfile
3import time3import time
4+import unittest
4from unittest.mock import patch5from unittest.mock import patch
5 6 
7+import torch
6import torch_npu8import torch_npu
9+import torch_npu.npu.graphs as npu_graphs
7from torch_npu.testing.common_utils import SkipIfNotGteCANNVersion, SupportedDevices10from torch_npu.testing.common_utils import SkipIfNotGteCANNVersion, SupportedDevices
8from torch_npu.testing.testcase import run_tests, TestCase11from torch_npu.testing.testcase import run_tests, TestCase
9 12 
10-import torch
11- 
12 13 
13def wait_until(predicate, timeout=5.0, interval=0.01):14def wait_until(predicate, timeout=5.0, interval=0.01):
14 deadline = time.time() + timeout15 deadline = time.time() + timeout
@@ -27,12 +28,58 @@ def _resolved_npu_device_index(tensor):
27 28 
28 29 
29class TestAclgraphDfx(TestCase):30class TestAclgraphDfx(TestCase):
31+ 
32+ def test_npugraph_tensor_ptr_spec_uses_raw_pointer_metadata(self):
33+ x = torch.arange(6, dtype=torch.float32).reshape(2, 3)
34+ 
35+ spec = npu_graphs._make_npugraph_tensor_ptr_spec(x)
36+ 
37+ self.assertEqual(spec[0], npu_graphs._NPUGRAPH_TENSOR_PTR_SPEC_MARKER)
38+ self.assertEqual(spec[1], x.data_ptr())
39+ self.assertEqual(spec[2], x.numel() * x.element_size())
40+ self.assertEqual(spec[3], tuple(x.shape))
41+ self.assertEqual(spec[4], x.dtype)
42+ 
43+ def test_materialize_npugraph_tensor_buffer_spec(self):
44+ expected = torch.arange(6, dtype=torch.float32).reshape(2, 3)
45+ buffer_spec = (
46+ npu_graphs._NPUGRAPH_TENSOR_BUFFER_SPEC_MARKER,
47+ bytearray(expected.numpy().tobytes()),
48+ tuple(expected.shape),
49+ expected.dtype,
50+ )
51+ 
52+ actual = npu_graphs._materialize_npugraph_tensor_arg(buffer_spec)
53+ 
54+ self.assertEqual(actual, expected)
55+ 
56+ def test_materialize_npugraph_tensor_buffer_spec_list(self):
57+ expected = [
58+ torch.arange(6, dtype=torch.float32).reshape(2, 3),
59+ torch.arange(4, dtype=torch.int32).reshape(2, 2),
60+ ]
61+ buffer_specs = [
62+ (
63+ npu_graphs._NPUGRAPH_TENSOR_BUFFER_SPEC_MARKER,
64+ bytearray(tensor.numpy().tobytes()),
65+ tuple(tensor.shape),
66+ tensor.dtype,
67+ )
68+ for tensor in expected
69+ ]
70+ 
71+ actual = npu_graphs._materialize_npugraph_tensor_arg(buffer_specs)
72+ 
73+ self.assertEqual(len(actual), len(expected))
74+ self.assertEqual(actual[0], expected[0])
75+ self.assertEqual(actual[1], expected[1])
76+ 
30 @SkipIfNotGteCANNVersion("8.5.0")77 @SkipIfNotGteCANNVersion("8.5.0")
31 @SupportedDevices(["Ascend910B", "Ascend910_93"])78 @SupportedDevices(["Ascend910B", "Ascend910_93"])
32 def test_print_npugraph_tensor(self):79 def test_print_npugraph_tensor(self):
33 torch.npu.set_device(0)80 torch.npu.set_device(0)
34 g = torch.npu.NPUGraph()81 g = torch.npu.NPUGraph()
35- x = torch.arange(6, dtype=torch.float32, device="npu").reshape(2, 3)82+ x = torch.arange(6, dtype=torch.float32, device='npu').reshape(2, 3)
36 83 
37 with patch("builtins.print") as mock_print:84 with patch("builtins.print") as mock_print:
38 with torch.npu.graph(g):85 with torch.npu.graph(g):
@@ -41,9 +88,7 @@ class TestAclgraphDfx(TestCase):
41 torch.npu.synchronize()88 torch.npu.synchronize()
42 self.assertTrue(wait_until(lambda: mock_print.call_count > 0))89 self.assertTrue(wait_until(lambda: mock_print.call_count > 0))
43 90 
44- printed_messages = [91+ printed_messages = [call.args[0] for call in mock_print.call_args_list if call.args]
45- call.args[0] for call in mock_print.call_args_list if call.args
46- ]
47 self.assertTrue(any("tensor=tensor(" in msg for msg in printed_messages))92 self.assertTrue(any("tensor=tensor(" in msg for msg in printed_messages))
48 self.assertTrue(any("shape=(2, 3)" in msg for msg in printed_messages))93 self.assertTrue(any("shape=(2, 3)" in msg for msg in printed_messages))
49 self.assertTrue(any("dtype=torch.float32" in msg for msg in printed_messages))94 self.assertTrue(any("dtype=torch.float32" in msg for msg in printed_messages))
@@ -53,7 +98,7 @@ class TestAclgraphDfx(TestCase):
53 def test_print_npugraph_tensor_with_default_message(self):98 def test_print_npugraph_tensor_with_default_message(self):
54 torch.npu.set_device(0)99 torch.npu.set_device(0)
55 g = torch.npu.NPUGraph()100 g = torch.npu.NPUGraph()
56- x = torch.arange(6, dtype=torch.float32, device="npu").reshape(2, 3)101+ x = torch.arange(6, dtype=torch.float32, device='npu').reshape(2, 3)
57 102 
58 with patch("builtins.print") as mock_print:103 with patch("builtins.print") as mock_print:
59 with torch.npu.graph(g):104 with torch.npu.graph(g):
@@ -62,9 +107,7 @@ class TestAclgraphDfx(TestCase):
62 torch.npu.synchronize()107 torch.npu.synchronize()
63 self.assertTrue(wait_until(lambda: mock_print.call_count > 0))108 self.assertTrue(wait_until(lambda: mock_print.call_count > 0))
64 109 
65- printed_messages = [110+ printed_messages = [call.args[0] for call in mock_print.call_args_list if call.args]
66- call.args[0] for call in mock_print.call_args_list if call.args
67- ]
68 self.assertTrue(any(msg.startswith("tensor(") for msg in printed_messages))111 self.assertTrue(any(msg.startswith("tensor(") for msg in printed_messages))
69 112 
70 @SkipIfNotGteCANNVersion("8.5.0")113 @SkipIfNotGteCANNVersion("8.5.0")
@@ -72,7 +115,7 @@ class TestAclgraphDfx(TestCase):
72 def test_print_npugraph_tensor_with_args(self):115 def test_print_npugraph_tensor_with_args(self):
73 torch.npu.set_device(0)116 torch.npu.set_device(0)
74 g = torch.npu.NPUGraph()117 g = torch.npu.NPUGraph()
75- x = torch.arange(6, dtype=torch.float32, device="npu").reshape(2, 3)118+ x = torch.arange(6, dtype=torch.float32, device='npu').reshape(2, 3)
76 119 
77 with patch("builtins.print") as mock_print:120 with patch("builtins.print") as mock_print:
78 with torch.npu.graph(g):121 with torch.npu.graph(g):
@@ -81,9 +124,7 @@ class TestAclgraphDfx(TestCase):
81 torch.npu.synchronize()124 torch.npu.synchronize()
82 self.assertTrue(wait_until(lambda: mock_print.call_count > 0))125 self.assertTrue(wait_until(lambda: mock_print.call_count > 0))
83 126 
84- printed_messages = [127+ printed_messages = [call.args[0] for call in mock_print.call_args_list if call.args]
85- call.args[0] for call in mock_print.call_args_list if call.args
86- ]
87 self.assertTrue(any("x=tensor(" in msg for msg in printed_messages))128 self.assertTrue(any("x=tensor(" in msg for msg in printed_messages))
88 self.assertTrue(any("shape=(2, 3)" in msg for msg in printed_messages))129 self.assertTrue(any("shape=(2, 3)" in msg for msg in printed_messages))
89 self.assertTrue(any("dtype=torch.float32" in msg for msg in printed_messages))130 self.assertTrue(any("dtype=torch.float32" in msg for msg in printed_messages))
@@ -94,17 +135,13 @@ class TestAclgraphDfx(TestCase):
94 torch.npu.set_device(0)135 torch.npu.set_device(0)
95 first_graph = torch.npu.NPUGraph()136 first_graph = torch.npu.NPUGraph()
96 second_graph = torch.npu.NPUGraph()137 second_graph = torch.npu.NPUGraph()
97- x = torch.arange(6, dtype=torch.float32, device="npu").reshape(2, 3)138+ x = torch.arange(6, dtype=torch.float32, device='npu').reshape(2, 3)
98 device_index = _resolved_npu_device_index(x)139 device_index = _resolved_npu_device_index(x)
99 140 
100 with tempfile.TemporaryDirectory() as tmpdir:141 with tempfile.TemporaryDirectory() as tmpdir:
101 save_path = os.path.join(tmpdir, "tensor.pt")142 save_path = os.path.join(tmpdir, "tensor.pt")
102- expected_counter_path = os.path.join(143+ expected_counter_path = os.path.join(tmpdir, f"tensor_device_{device_index}_0.pt")
103- tmpdir, f"tensor_device_{device_index}_0.pt"144+ expected_second_counter_path = os.path.join(tmpdir, f"tensor_device_{device_index}_1.pt")
104- )
105- expected_second_counter_path = os.path.join(
106- tmpdir, f"tensor_device_{device_index}_1.pt"
107- )
108 145 
109 with torch.npu.graph(first_graph):146 with torch.npu.graph(first_graph):
110 torch.ops.npu.save_npugraph_tensor(x, save_path=save_path)147 torch.ops.npu.save_npugraph_tensor(x, save_path=save_path)
@@ -117,9 +154,7 @@ class TestAclgraphDfx(TestCase):
117 torch.ops.npu.save_npugraph_tensor(x, save_path=save_path)154 torch.ops.npu.save_npugraph_tensor(x, save_path=save_path)
118 second_graph.replay()155 second_graph.replay()
119 torch.npu.synchronize()156 torch.npu.synchronize()
120- self.assertTrue(157+ self.assertTrue(wait_until(lambda: os.path.exists(expected_second_counter_path)))
121- wait_until(lambda: os.path.exists(expected_second_counter_path))
122- )
123 self.assertEqual(torch.load(expected_second_counter_path), x.cpu())158 self.assertEqual(torch.load(expected_second_counter_path), x.cpu())
124 159 
125 @SkipIfNotGteCANNVersion("8.5.0")160 @SkipIfNotGteCANNVersion("8.5.0")
@@ -128,23 +163,17 @@ class TestAclgraphDfx(TestCase):
128 torch.npu.set_device(0)163 torch.npu.set_device(0)
129 first_graph = torch.npu.NPUGraph()164 first_graph = torch.npu.NPUGraph()
130 second_graph = torch.npu.NPUGraph()165 second_graph = torch.npu.NPUGraph()
131- x = torch.arange(6, dtype=torch.float32, device="npu").reshape(2, 3)166+ x = torch.arange(6, dtype=torch.float32, device='npu').reshape(2, 3)
132- y = torch.arange(6, dtype=torch.float32, device="npu").reshape(2, 3) + 1167+ y = torch.arange(6, dtype=torch.float32, device='npu').reshape(2, 3) + 1
133 device_index = _resolved_npu_device_index(x)168 device_index = _resolved_npu_device_index(x)
134 169 
135 with tempfile.TemporaryDirectory() as tmpdir:170 with tempfile.TemporaryDirectory() as tmpdir:
136 save_path = os.path.join(tmpdir, "tensor.pt")171 save_path = os.path.join(tmpdir, "tensor.pt")
137- expected_overwrite_path = os.path.join(172+ expected_overwrite_path = os.path.join(tmpdir, f"tensor_device_{device_index}.pt")
138- tmpdir, f"tensor_device_{device_index}.pt"173+ unexpected_counter_path = os.path.join(tmpdir, f"tensor_device_{device_index}_0.pt")
139- )
140- unexpected_counter_path = os.path.join(
141- tmpdir, f"tensor_device_{device_index}_0.pt"
142- )
143 174 
144 with torch.npu.graph(first_graph):175 with torch.npu.graph(first_graph):
145- torch.ops.npu.save_npugraph_tensor(176+ torch.ops.npu.save_npugraph_tensor(x, save_path=save_path, overwrite=True)
146- x, save_path=save_path, overwrite=True
147- )
148 first_graph.replay()177 first_graph.replay()
149 torch.npu.synchronize()178 torch.npu.synchronize()
150 self.assertTrue(wait_until(lambda: os.path.exists(expected_overwrite_path)))179 self.assertTrue(wait_until(lambda: os.path.exists(expected_overwrite_path)))
@@ -152,9 +181,7 @@ class TestAclgraphDfx(TestCase):
152 self.assertEqual(torch.load(expected_overwrite_path), x.cpu())181 self.assertEqual(torch.load(expected_overwrite_path), x.cpu())
153 182 
154 with torch.npu.graph(second_graph):183 with torch.npu.graph(second_graph):
155- torch.ops.npu.save_npugraph_tensor(184+ torch.ops.npu.save_npugraph_tensor(y, save_path=save_path, overwrite=True)
156- y, save_path=save_path, overwrite=True
157- )
158 second_graph.replay()185 second_graph.replay()
159 torch.npu.synchronize()186 torch.npu.synchronize()
160 self.assertEqual(torch.load(expected_overwrite_path), y.cpu())187 self.assertEqual(torch.load(expected_overwrite_path), y.cpu())
@@ -164,7 +191,7 @@ class TestAclgraphDfx(TestCase):
164 def test_save_npugraph_tensor_with_default_save_path(self):191 def test_save_npugraph_tensor_with_default_save_path(self):
165 torch.npu.set_device(0)192 torch.npu.set_device(0)
166 g = torch.npu.NPUGraph()193 g = torch.npu.NPUGraph()
167- x = torch.arange(6, dtype=torch.float32, device="npu").reshape(2, 3)194+ x = torch.arange(6, dtype=torch.float32, device='npu').reshape(2, 3)
168 device_index = _resolved_npu_device_index(x)195 device_index = _resolved_npu_device_index(x)
169 196 
170 with tempfile.TemporaryDirectory() as tmpdir:197 with tempfile.TemporaryDirectory() as tmpdir:
@@ -178,8 +205,7 @@ class TestAclgraphDfx(TestCase):
178 205 
179 def default_saved_files():206 def default_saved_files():
180 return [207 return [
181- file_name208+ file_name for file_name in os.listdir(tmpdir)
182- for file_name in os.listdir(tmpdir)
183 if file_name.startswith("tensor_") and file_name.endswith(".pt")209 if file_name.startswith("tensor_") and file_name.endswith(".pt")
184 ]210 ]
185 211 
@@ -195,20 +221,16 @@ class TestAclgraphDfx(TestCase):
195 def test_save_npugraph_tensor_tensor_list(self):221 def test_save_npugraph_tensor_tensor_list(self):
196 torch.npu.set_device(0)222 torch.npu.set_device(0)
197 g = torch.npu.NPUGraph()223 g = torch.npu.NPUGraph()
198- x = torch.arange(6, dtype=torch.float32, device="npu").reshape(2, 3)224+ x = torch.arange(6, dtype=torch.float32, device='npu').reshape(2, 3)
199- y = torch.arange(4, dtype=torch.float16, device="npu").reshape(2, 2)225+ y = torch.arange(4, dtype=torch.float16, device='npu').reshape(2, 2)
200 device_index = _resolved_npu_device_index(x)226 device_index = _resolved_npu_device_index(x)
201 227 
202 with tempfile.TemporaryDirectory() as tmpdir:228 with tempfile.TemporaryDirectory() as tmpdir:
203 save_path = os.path.join(tmpdir, "tensor_list.pt")229 save_path = os.path.join(tmpdir, "tensor_list.pt")
204- expected_path = os.path.join(230+ expected_path = os.path.join(tmpdir, f"tensor_list_device_{device_index}_0.pt")
205- tmpdir, f"tensor_list_device_{device_index}_0.pt"
206- )
207 231 
208 with torch.npu.graph(g):232 with torch.npu.graph(g):
209- torch.ops.npu.save_npugraph_tensor.tensor_list(233+ torch.ops.npu.save_npugraph_tensor.tensor_list([x, y], save_path=save_path)
210- [x, y], save_path=save_path
211- )
212 g.replay()234 g.replay()
213 torch.npu.synchronize()235 torch.npu.synchronize()
214 self.assertTrue(wait_until(lambda: os.path.exists(expected_path)))236 self.assertTrue(wait_until(lambda: os.path.exists(expected_path)))
@@ -219,5 +241,5 @@ class TestAclgraphDfx(TestCase):
219 self.assertEqual(saved[1], y.cpu())241 self.assertEqual(saved[1], y.cpu())
220 242 
221 243 
222-if __name__ == "__main__":244+if __name__ == '__main__':
223- run_tests()245+ run_tests()
@@ -1,7 +1,12 @@
1#include <deque>1#include <deque>
2+#include <cstdint>
3+#include <cstring>
2#include <thread>4#include <thread>
5+#include <utility>
3#include <vector>6#include <vector>
4 7 
8+#include <ATen/ATen.h>
9+ 
5#include "torch_npu/csrc/npu/Graph.h"10#include "torch_npu/csrc/npu/Graph.h"
6 11 
7#include "op_plugin/OpApiInterface.h"12#include "op_plugin/OpApiInterface.h"
@@ -19,7 +24,8 @@ constexpr auto pendingCallRetryInterval = std::chrono::milliseconds(1);
19static ThreadArgs* threadArgs = nullptr;24static ThreadArgs* threadArgs = nullptr;
20static uint64_t threadId = -1;25static uint64_t threadId = -1;
21static std::mutex pendingCallbacksMutex;26static std::mutex pendingCallbacksMutex;
22-static std::deque<PyFuncStruct*> pendingCallbacksQueue;27+using PendingCallbackEntry = std::pair<PendingCallPayload*, std::vector<at::Tensor>>;
28+static std::deque<PendingCallbackEntry> pendingCallbacksQueue;
23static std::atomic<bool> pendingCallScheduled{false};29static std::atomic<bool> pendingCallScheduled{false};
24 30 
25void *process_callback(void *arg)31void *process_callback(void *arg)
@@ -60,6 +66,9 @@ void LaunchCallFunc(void *userData)
60 66 
61namespace {67namespace {
62 68 
69+constexpr const char* kNpuGraphTensorPtrSpecMarker = "host_tensor_ptr";
70+constexpr const char* kNpuGraphTensorBufferSpecMarker = "host_tensor_buffer";
71+ 
63int PendingCallHandler(void *arg);72int PendingCallHandler(void *arg);
64 73 
65bool TrySchedulePendingCall()74bool TrySchedulePendingCall()
@@ -82,25 +91,215 @@ void StartPendingCallRetryThread()
82 }).detach();91 }).detach();
83}92}
84 93 
94+bool IsNpuGraphTensorPtrSpec(PyObject* obj)
95+{
96+ if (!PyTuple_Check(obj) || PyTuple_GET_SIZE(obj) != 5) {
97+ return false;
98+ }
99+ PyObject* marker = PyTuple_GET_ITEM(obj, 0);
100+ return PyUnicode_Check(marker) &&
101+ PyUnicode_CompareWithASCIIString(marker, kNpuGraphTensorPtrSpecMarker) == 0;
102+}
103+ 
104+bool CollectNpuGraphTensorPtrSpec(PyObject* obj, PendingCallPayload* payload)
105+{
106+ PyObject* ptrObj = PyTuple_GET_ITEM(obj, 1);
107+ PyObject* nbytesObj = PyTuple_GET_ITEM(obj, 2);
108+ PyObject* shapeObj = PyTuple_GET_ITEM(obj, 3);
109+ PyObject* dtypeObj = PyTuple_GET_ITEM(obj, 4);
110+ 
111+ auto dataPtr = static_cast<uintptr_t>(PyLong_AsUnsignedLongLong(ptrObj));
112+ if (PyErr_Occurred()) {
113+ return false;
114+ }
115+ auto nbytes = static_cast<Py_ssize_t>(PyLong_AsSsize_t(nbytesObj));
116+ if (PyErr_Occurred()) {
117+ return false;
118+ }
119+ if (nbytes < 0) {
120+ PyErr_SetString(PyExc_ValueError, "npugraph tensor buffer size must be non-negative");
121+ return false;
122+ }
123+ payload->pendingTensorData.emplace_back(dataPtr, nbytes, shapeObj, dtypeObj);
124+ return true;
125+}
126+ 
127+bool CollectPendingTensorData(PyObject* obj, PendingCallPayload* payload)
128+{
129+ if (IsNpuGraphTensorPtrSpec(obj)) {
130+ return CollectNpuGraphTensorPtrSpec(obj, payload);
131+ }
132+ 
133+ if (PyTuple_Check(obj)) {
134+ Py_ssize_t size = PyTuple_GET_SIZE(obj);
135+ for (Py_ssize_t i = 0; i < size; ++i) {
136+ if (!CollectPendingTensorData(PyTuple_GET_ITEM(obj, i), payload)) {
137+ return false;
138+ }
139+ }
140+ return true;
141+ }
142+ 
143+ if (PyList_Check(obj)) {
144+ Py_ssize_t size = PyList_GET_SIZE(obj);
145+ for (Py_ssize_t i = 0; i < size; ++i) {
146+ if (!CollectPendingTensorData(PyList_GET_ITEM(obj, i), payload)) {
147+ return false;
148+ }
149+ }
150+ return true;
151+ }
152+ 
153+ return true;
154+}
155+ 
156+void CopyPendingTensorData(PendingCallPayload* payload, std::vector<at::Tensor>& copiedTensors)
157+{
158+ try {
159+ copiedTensors.clear();
160+ copiedTensors.reserve(payload->pendingTensorData.size());
161+ for (const auto& tensorData : payload->pendingTensorData) {
162+ at::Tensor copiedTensor = at::empty(
163+ {static_cast<int64_t>(tensorData.nbytes)},
164+ at::TensorOptions().dtype(at::kByte).device(at::kCPU));
165+ if (tensorData.nbytes > 0) {
166+ std::memcpy(
167+ copiedTensor.data_ptr(),
168+ reinterpret_cast<void*>(tensorData.dataPtr),
169+ static_cast<size_t>(tensorData.nbytes));
170+ }
171+ copiedTensors.emplace_back(std::move(copiedTensor));
172+ }
173+ } catch (...) {
174+ copiedTensors.clear();
175+ }
176+}
177+ 
178+PyObject* MaterializeNpuGraphTensorBufferSpec(
179+ PendingCallPayload* payload,
180+ const std::vector<at::Tensor>& copiedTensors,
181+ size_t& tensorDataIndex)
182+{
183+ if (tensorDataIndex >= payload->pendingTensorData.size() ||
184+ tensorDataIndex >= copiedTensors.size()) {
185+ PyErr_SetString(PyExc_RuntimeError, "npugraph tensor buffer index is out of range");
186+ return nullptr;
187+ }
188+ auto& tensorData = payload->pendingTensorData[tensorDataIndex];
189+ const auto& copiedTensor = copiedTensors[tensorDataIndex++];
190+ if (!copiedTensor.defined()) {
191+ PyErr_NoMemory();
192+ return nullptr;
193+ }
194+ 
195+ PyObject* buffer = PyMemoryView_FromMemory(
196+ static_cast<char*>(copiedTensor.data_ptr()),
197+ tensorData.nbytes,
198+ PyBUF_WRITE);
199+ if (buffer == nullptr) {
200+ return nullptr;
201+ }
202+ 
203+ PyObject* marker = PyUnicode_FromString(kNpuGraphTensorBufferSpecMarker);
204+ if (marker == nullptr) {
205+ Py_DECREF(buffer);
206+ return nullptr;
207+ }
208+ PyObject* bufferSpec = PyTuple_Pack(4, marker, buffer, tensorData.shape, tensorData.dtype);
209+ Py_DECREF(marker);
210+ Py_DECREF(buffer);
211+ return bufferSpec;
212+}
213+ 
214+PyObject* MaterializePendingArg(
215+ PyObject* obj,
216+ PendingCallPayload* payload,
217+ const std::vector<at::Tensor>& copiedTensors,
218+ size_t& tensorDataIndex)
219+{
220+ if (IsNpuGraphTensorPtrSpec(obj)) {
221+ return MaterializeNpuGraphTensorBufferSpec(payload, copiedTensors, tensorDataIndex);
222+ }
223+ 
224+ if (PyTuple_Check(obj)) {
225+ Py_ssize_t size = PyTuple_GET_SIZE(obj);
226+ PyObject* tuple = PyTuple_New(size);
227+ if (tuple == nullptr) {
228+ return nullptr;
229+ }
230+ for (Py_ssize_t i = 0; i < size; ++i) {
231+ PyObject* item = MaterializePendingArg(PyTuple_GET_ITEM(obj, i), payload, copiedTensors, tensorDataIndex);
232+ if (item == nullptr) {
233+ Py_DECREF(tuple);
234+ return nullptr;
235+ }
236+ PyTuple_SET_ITEM(tuple, i, item);
237+ }
238+ return tuple;
239+ }
240+ 
241+ if (PyList_Check(obj)) {
242+ Py_ssize_t size = PyList_GET_SIZE(obj);
243+ PyObject* list = PyList_New(size);
244+ if (list == nullptr) {
245+ return nullptr;
246+ }
247+ for (Py_ssize_t i = 0; i < size; ++i) {
248+ PyObject* item = MaterializePendingArg(PyList_GET_ITEM(obj, i), payload, copiedTensors, tensorDataIndex);
249+ if (item == nullptr) {
250+ Py_DECREF(list);
251+ return nullptr;
252+ }
253+ PyList_SET_ITEM(list, i, item);
254+ }
255+ return list;
256+ }
257+ 
258+ Py_INCREF(obj);
259+ return obj;
260+}
261+ 
262+PyObject* MaterializePendingArgs(PendingCallPayload* payload, const std::vector<at::Tensor>& copiedTensors)
263+{
264+ size_t tensorDataIndex = 0;
265+ PyObject* materializedArgs = MaterializePendingArg(
266+ payload->pyFuncData.pyFuncArgs, payload, copiedTensors, tensorDataIndex);
267+ if (materializedArgs == nullptr) {
268+ PyErr_WriteUnraisable(payload->pyFuncData.pyFunc);
269+ PyErr_Clear();
270+ return nullptr;
271+ }
272+ return materializedArgs;
273+}
274+ 
85int PendingCallHandler(void *arg)275int PendingCallHandler(void *arg)
86{276{
87- std::deque<PyFuncStruct*> readyCallbacks;277+ std::deque<PendingCallbackEntry> readyCallbacks;
88 {278 {
89 std::lock_guard<std::mutex> lock(pendingCallbacksMutex);279 std::lock_guard<std::mutex> lock(pendingCallbacksMutex);
90 pendingCallScheduled.store(false, std::memory_order_release);280 pendingCallScheduled.store(false, std::memory_order_release);
91 readyCallbacks.swap(pendingCallbacksQueue);281 readyCallbacks.swap(pendingCallbacksQueue);
92 }282 }
93 283 
94- for (auto* data : readyCallbacks) {284+ for (auto& callback : readyCallbacks) {
95- if (data == nullptr) {285+ auto* payload = callback.first;
286+ auto& copiedTensors = callback.second;
287+ if (payload == nullptr) {
96 continue;288 continue;
97 }289 }
98- PyObject* result = PyObject_CallObject(data->pyFunc, data->pyFuncArgs);290+ PyObject* materializedArgs = MaterializePendingArgs(payload, copiedTensors);
291+ if (materializedArgs == nullptr) {
292+ copiedTensors.clear();
293+ continue;
294+ }
295+ PyObject* result = PyObject_CallObject(payload->pyFuncData.pyFunc, materializedArgs);
99 if (result != nullptr) {296 if (result != nullptr) {
100 Py_XDECREF(result);297 Py_XDECREF(result);
101 } else {298 } else {
102- PyErr_WriteUnraisable(data->pyFunc);299+ PyErr_WriteUnraisable(payload->pyFuncData.pyFunc);
103 }300 }
301+ Py_DECREF(materializedArgs);
302+ copiedTensors.clear();
104 }303 }
105 304 
106 bool shouldScheduleAgain = false;305 bool shouldScheduleAgain = false;
@@ -121,15 +320,18 @@ int PendingCallHandler(void *arg)
121 320 
122void LaunchCallbackViaPendingCall(void *userData)321void LaunchCallbackViaPendingCall(void *userData)
123{322{
124- auto* data = static_cast<PyFuncStruct*>(userData);323+ auto* payload = static_cast<PendingCallPayload*>(userData);
125- if (data == nullptr) {324+ if (payload == nullptr) {
126 return;325 return;
127 }326 }
128 327 
328+ std::vector<at::Tensor> copiedTensors;
329+ CopyPendingTensorData(payload, copiedTensors);
330+ 
129 bool shouldSchedule = false;331 bool shouldSchedule = false;
130 {332 {
131 std::lock_guard<std::mutex> lock(pendingCallbacksMutex);333 std::lock_guard<std::mutex> lock(pendingCallbacksMutex);
132- pendingCallbacksQueue.emplace_back(data);334+ pendingCallbacksQueue.emplace_back(payload, std::move(copiedTensors));
133 if (!pendingCallScheduled.exchange(true, std::memory_order_acq_rel)) {335 if (!pendingCallScheduled.exchange(true, std::memory_order_acq_rel)) {
134 shouldSchedule = true;336 shouldSchedule = true;
135 }337 }
@@ -340,9 +542,12 @@ void TORCH_NPU_API THNPGraph_init(PyObject* module) {
340 auto func = (*py_func).ptr();542 auto func = (*py_func).ptr();
341 auto userDataList = (*py_data).ptr();543 auto userDataList = (*py_data).ptr();
342 auto stream = THNPUtils_PyObject_to_NPUStream((*py_stream).ptr());544 auto stream = THNPUtils_PyObject_to_NPUStream((*py_stream).ptr());
343- auto data = std::make_unique<PyFuncStruct>(func, userDataList);545+ auto payload = std::make_unique<PendingCallPayload>(func, userDataList);
344- c10_npu::launch_host_func(stream, LaunchCallbackViaPendingCall, data.get());546+ if (!CollectPendingTensorData(userDataList, payload.get())) {
345- (void)data.release();547+ throw py::error_already_set();
548+ }
549+ c10_npu::launch_host_func(stream, LaunchCallbackViaPendingCall, payload.get());
550+ (void)payload.release();
346 })551 })
347 .def("_subscribe_report", [](py::object py_stream) {552 .def("_subscribe_report", [](py::object py_stream) {
348 auto stream = (*py_stream).ptr();553 auto stream = (*py_stream).ptr();
@@ -1,4 +1,6 @@
1#include <torch/csrc/python_headers.h>1#include <torch/csrc/python_headers.h>
2+#include <cstdint>
3+#include <vector>
2#include <pybind11/chrono.h>4#include <pybind11/chrono.h>
3#include <torch/csrc/jit/python/pybind_utils.h>5#include <torch/csrc/jit/python/pybind_utils.h>
4#include <torch/csrc/utils/pybind.h>6#include <torch/csrc/utils/pybind.h>
@@ -7,6 +9,20 @@
7#include "third_party/acl/inc/acl/acl_rt.h"9#include "third_party/acl/inc/acl/acl_rt.h"
8#include "third_party/acl/inc/acl/acl_sk.h"10#include "third_party/acl/inc/acl/acl_sk.h"
9 11 
12+struct PendingTensorData {
13+ PendingTensorData(uintptr_t dataPtr, Py_ssize_t nbytes, PyObject* shape, PyObject* dtype)
14+ : dataPtr(dataPtr), nbytes(nbytes), shape(shape), dtype(dtype)
15+ {
16+ Py_XINCREF(shape);
17+ Py_XINCREF(dtype);
18+ }
19+ 
20+ uintptr_t dataPtr = 0;
21+ Py_ssize_t nbytes = 0;
22+ PyObject* shape = nullptr;
23+ PyObject* dtype = nullptr;
24+};
25+ 
10struct PyFuncStruct {26struct PyFuncStruct {
11 PyFuncStruct(PyObject *pyFunc, PyObject *pyFuncArgs)27 PyFuncStruct(PyObject *pyFunc, PyObject *pyFuncArgs)
12 : pyFunc(pyFunc), pyFuncArgs(pyFuncArgs)28 : pyFunc(pyFunc), pyFuncArgs(pyFuncArgs)
@@ -25,6 +41,25 @@ struct PyFuncStruct {
25 PyObject* pyFuncArgs = nullptr;41 PyObject* pyFuncArgs = nullptr;
26};42};
27 43 
44+struct PendingCallPayload {
45+ PendingCallPayload(PyObject* pyFunc, PyObject* pyFuncArgs)
46+ : pyFuncData(pyFunc, pyFuncArgs)
47+ {
48+ }
49+ 
50+ ~PendingCallPayload()
51+ {
52+ Py_CLEAR(pyFuncData.pyFuncArgs);
53+ for (auto& tensorData : pendingTensorData) {
54+ Py_XDECREF(tensorData.shape);
55+ Py_XDECREF(tensorData.dtype);
56+ }
57+ }
58+ 
59+ PyFuncStruct pyFuncData;
60+ std::vector<PendingTensorData> pendingTensorData;
61+};
62+ 
28struct ThreadArgs {63struct ThreadArgs {
29 ThreadArgs(aclrtContext context, bool exitFlag)64 ThreadArgs(aclrtContext context, bool exitFlag)
30 : context(context), exitFlag(exitFlag) {}65 : context(context), exitFlag(exitFlag) {}
@@ -122,6 +122,8 @@ _save_npugraph_tensor_lock = threading.Lock()
122_save_npugraph_tensor_counters = {}122_save_npugraph_tensor_counters = {}
123_save_tensor_streams: Dict[int, "torch_npu.npu.Stream"] = {}123_save_tensor_streams: Dict[int, "torch_npu.npu.Stream"] = {}
124_save_tensor_stream_lock = threading.Lock()124_save_tensor_stream_lock = threading.Lock()
125+_NPUGRAPH_TENSOR_PTR_SPEC_MARKER = "host_tensor_ptr"
126+_NPUGRAPH_TENSOR_BUFFER_SPEC_MARKER = "host_tensor_buffer"
125 127 
126 128 
127def _get_save_tensor_stream(device_index: int):129def _get_save_tensor_stream(device_index: int):
@@ -167,6 +169,7 @@ def _build_save_npugraph_tensor_path(save_path=None, device_index=None, overwrit
167 169 
168 170 
169def _print_callback_pending(tensor_name, tensor_arg):171def _print_callback_pending(tensor_name, tensor_arg):
172+ tensor_arg = _materialize_npugraph_tensor_arg(tensor_arg)
170 output = str(tensor_arg)173 output = str(tensor_arg)
171 if tensor_name is not None:174 if tensor_name is not None:
172 output = f"{tensor_name}={output}"175 output = f"{tensor_name}={output}"
@@ -175,9 +178,35 @@ def _print_callback_pending(tensor_name, tensor_arg):
175 178 
176 179 
177def _save_callback_pending(tensor_arg, str_arg):180def _save_callback_pending(tensor_arg, str_arg):
181+ tensor_arg = _materialize_npugraph_tensor_arg(tensor_arg)
178 torch.save(tensor_arg, str_arg)182 torch.save(tensor_arg, str_arg)
179 183 
180 184 
185+def _make_npugraph_tensor_ptr_spec(tensor):
186+ return (
187+ _NPUGRAPH_TENSOR_PTR_SPEC_MARKER,
188+ tensor.data_ptr(),
189+ tensor.numel() * tensor.element_size(),
190+ tuple(tensor.shape),
191+ tensor.dtype,
192+ )
193+ 
194+ 
195+def _materialize_npugraph_tensor_arg(tensor_arg):
196+ if (
197+ isinstance(tensor_arg, tuple)
198+ and len(tensor_arg) == 4
199+ and tensor_arg[0] == _NPUGRAPH_TENSOR_BUFFER_SPEC_MARKER
200+ ):
201+ _, buffer, shape, dtype = tensor_arg
202+ if len(buffer) == 0:
203+ return torch.empty(shape, dtype=dtype)
204+ return torch.frombuffer(buffer, dtype=dtype).reshape(shape)
205+ if isinstance(tensor_arg, list):
206+ return [_materialize_npugraph_tensor_arg(arg) for arg in tensor_arg]
207+ return tensor_arg
208+ 
209+ 
181def _validate_tensor_list(inputs):210def _validate_tensor_list(inputs):
182 if not isinstance(inputs, (list, tuple)):211 if not isinstance(inputs, (list, tuple)):
183 raise TypeError(f"input must be Tensor or TensorList, but got {type(inputs).__name__}")212 raise TypeError(f"input must be Tensor or TensorList, but got {type(inputs).__name__}")
@@ -197,9 +226,12 @@ def _print_npugraph_tensor_impl(input, tensor_name=None):
197 return226 return
198 227 
199 device = input.device228 device = input.device
200- if device.type != "npu":229+ if device.type == "cpu":
201 _print_callback_pending(tensor_name, input)230 _print_callback_pending(tensor_name, input)
202 return231 return
232+
233+ if device.type != "npu":
234+ return
203 235 
204 device_index = device.index236 device_index = device.index
205 save_stream = _get_save_tensor_stream(device_index)237 save_stream = _get_save_tensor_stream(device_index)
@@ -212,7 +244,8 @@ def _print_npugraph_tensor_impl(input, tensor_name=None):
212 with torch.npu.stream(save_stream):244 with torch.npu.stream(save_stream):
213 # Wait for the original stream to complete before D2H245 # Wait for the original stream to complete before D2H
214 event1.wait()246 event1.wait()
215- cpu_arg = input.to("cpu", non_blocking=True)247+ cpu_tensor = input.to("cpu", non_blocking=True)
248+ cpu_arg = _make_npugraph_tensor_ptr_spec(cpu_tensor)
216 # Get current stream inside context (which is save_stream)249 # Get current stream inside context (which is save_stream)
217 current_stream = torch.npu.current_stream()250 current_stream = torch.npu.current_stream()
218 torch_npu.npu._launch_host_func_pending(251 torch_npu.npu._launch_host_func_pending(
@@ -232,9 +265,12 @@ def _save_npugraph_tensor_impl(input, save_path=None, overwrite=False):
232 return265 return
233 266 
234 device = input.device267 device = input.device
235- if device.type != "npu":268+ if device.type == "cpu":
236 torch.save(input, _build_save_npugraph_tensor_path(save_path, overwrite=overwrite))269 torch.save(input, _build_save_npugraph_tensor_path(save_path, overwrite=overwrite))
237 return270 return
271+
272+ if device.type != "npu":
273+ return
238 274 
239 device_index = device.index275 device_index = device.index
240 save_stream = _get_save_tensor_stream(device_index)276 save_stream = _get_save_tensor_stream(device_index)
@@ -248,7 +284,8 @@ def _save_npugraph_tensor_impl(input, save_path=None, overwrite=False):
248 with torch.npu.stream(save_stream):284 with torch.npu.stream(save_stream):
249 # Wait for the original stream to complete before D2H285 # Wait for the original stream to complete before D2H
250 event1.wait()286 event1.wait()
251- cpu_arg = input.to("cpu", non_blocking=True)287+ cpu_tensor = input.to("cpu", non_blocking=True)
288+ cpu_arg = _make_npugraph_tensor_ptr_spec(cpu_tensor)
252 # Get current stream inside context (which is save_stream)289 # Get current stream inside context (which is save_stream)
253 current_stream = torch.npu.current_stream()290 current_stream = torch.npu.current_stream()
254 torch_npu.npu._launch_host_func_pending(291 torch_npu.npu._launch_host_func_pending(
@@ -265,9 +302,12 @@ def _save_npugraph_tensor_impl(input, save_path=None, overwrite=False):
265 302 
266def _save_npugraph_tensor_tensor_list_impl(input, save_path=None, overwrite=False):303def _save_npugraph_tensor_tensor_list_impl(input, save_path=None, overwrite=False):
267 device = _validate_tensor_list(input)304 device = _validate_tensor_list(input)
268- if device.type != "npu":305+ if device.type == "cpu":
269 torch.save(list(input), _build_save_npugraph_tensor_path(save_path, overwrite=overwrite))306 torch.save(list(input), _build_save_npugraph_tensor_path(save_path, overwrite=overwrite))
270 return307 return
308+
309+ if device.type != "npu":
310+ return
271 311 
272 device_index = device.index312 device_index = device.index
273 save_stream = _get_save_tensor_stream(device_index)313 save_stream = _get_save_tensor_stream(device_index)
@@ -281,7 +321,8 @@ def _save_npugraph_tensor_tensor_list_impl(input, save_path=None, overwrite=Fals
281 with torch.npu.stream(save_stream):321 with torch.npu.stream(save_stream):
282 # Wait for the original stream to complete before D2H322 # Wait for the original stream to complete before D2H
283 event1.wait()323 event1.wait()
284- cpu_args = [tensor.to("cpu", non_blocking=True) for tensor in input]324+ cpu_tensors = [tensor.to("cpu", non_blocking=True) for tensor in input]
325+ cpu_args = [_make_npugraph_tensor_ptr_spec(tensor) for tensor in cpu_tensors]
285 # Get current stream inside context (which is save_stream)326 # Get current stream inside context (which is save_stream)
286 current_stream = torch.npu.current_stream()327 current_stream = torch.npu.current_stream()
287 torch_npu.npu._launch_host_func_pending(328 torch_npu.npu._launch_host_func_pending(
@@ -329,7 +370,7 @@ if not hasattr(torch.ops.npu, "print_npugraph_tensor"):
329 _npu_lib.define("print_npugraph_tensor(Tensor input, *, str? tensor_name=None) -> ()")370 _npu_lib.define("print_npugraph_tensor(Tensor input, *, str? tensor_name=None) -> ()")
330 371 
331 _npu_lib.impl("print_npugraph_tensor", _print_npugraph_tensor_impl, "PrivateUse1")372 _npu_lib.impl("print_npugraph_tensor", _print_npugraph_tensor_impl, "PrivateUse1")
332- _npu_lib.impl("print_npugraph_tensor", lambda input, *, tensor_name=None: None, "CPU")373+ _npu_lib.impl("print_npugraph_tensor", _print_npugraph_tensor_impl, "CPU")
333 374 
334 has_side_effect(torch.ops.npu.print_npugraph_tensor.default)375 has_side_effect(torch.ops.npu.print_npugraph_tensor.default)
335 376 
@@ -343,9 +384,9 @@ if not hasattr(torch.ops.npu, "save_npugraph_tensor"):
343 _npu_lib.define("save_npugraph_tensor.tensor_list(Tensor[] input, *, str? save_path=None, bool overwrite=False) -> ()")384 _npu_lib.define("save_npugraph_tensor.tensor_list(Tensor[] input, *, str? save_path=None, bool overwrite=False) -> ()")
344 385 
345 _npu_lib.impl("save_npugraph_tensor", _save_npugraph_tensor_impl, "PrivateUse1")386 _npu_lib.impl("save_npugraph_tensor", _save_npugraph_tensor_impl, "PrivateUse1")
346- _npu_lib.impl("save_npugraph_tensor", lambda input, *, save_path=None, overwrite=False: None, "CPU")387+ _npu_lib.impl("save_npugraph_tensor", _save_npugraph_tensor_impl, "CPU")
347 _npu_lib.impl("save_npugraph_tensor.tensor_list", _save_npugraph_tensor_tensor_list_impl, "PrivateUse1")388 _npu_lib.impl("save_npugraph_tensor.tensor_list", _save_npugraph_tensor_tensor_list_impl, "PrivateUse1")
348- _npu_lib.impl("save_npugraph_tensor.tensor_list", lambda input, *, save_path=None, overwrite=False: None, "CPU")389+ _npu_lib.impl("save_npugraph_tensor.tensor_list", _save_npugraph_tensor_tensor_list_impl, "CPU")
349 390 
350 has_side_effect(torch.ops.npu.save_npugraph_tensor.default)391 has_side_effect(torch.ops.npu.save_npugraph_tensor.default)
351 has_side_effect(torch.ops.npu.save_npugraph_tensor.tensor_list)392 has_side_effect(torch.ops.npu.save_npugraph_tensor.tensor_list)