Skip to content

Commit 0e5c170

Browse files
authored
fix(jax): skip padding for non-XLA SavedModels (deepmodeling#5602)
## Summary - detect whether a JAX SavedModel contains `XlaCallModule` during C++ initialization - keep the dynamic atom-count padding only for XLA-compiled lower calls - pass exact `nall_real` shapes for non-XLA SavedModels This is mainly to support deepmodeling#5598: padding only has value for XLA static-shape execution and otherwise changes the non-XLA inference shape unnecessarily. ## Tests - `git diff --check` - `cmake --build source/build --target deepmd_cc -j2` - `ruff check .` - `ruff format --check .` Not run: JAX C++ SavedModel runtime test, because local `source/tests/infer/deeppot_dpa.savedmodel` is not available. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved inference stability and correctness by applying neighbor-list input padding only for models that benefit from it. * Updated tensor sizing for neighbor-list computations (including coordinate/atom-type and mapping shapes) to use the appropriate atom count, reducing the risk of shape mismatches and unnecessary overhead. * Enhanced detection of whether the loaded computation graph uses XLA-style compilation to drive the padding behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 54c64ad commit 0e5c170

2 files changed

Lines changed: 283 additions & 13 deletions

File tree

source/api_cc/include/DeepPotJAX.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ class DeepPotJAX : public DeepPotBackend {
201201
bool do_message_passing;
202202
// has default fparam
203203
bool has_default_fparam_;
204+
// whether SavedModel execution goes through XLA and benefits from shape
205+
// padding; true for JAX/jax2tf XlaCallModule and TF2 jit_compile exports
206+
bool uses_xla_compilation_ = false;
204207
// padding to nall
205208
int padding_to_nall = 0;
206209
// padding for nloc

source/api_cc/src/DeepPotJAX.cc

Lines changed: 280 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
#include <array>
1010
#include <cstdint>
1111
#include <cstdio>
12+
#include <cstring>
1213
#include <iostream>
1314
#include <numeric>
1415
#include <ostream>
1516
#include <stdexcept>
17+
#include <string>
1618
#include <vector>
1719

1820
#include "common.h"
@@ -48,6 +50,265 @@ inline void find_function(TF_Function*& found_func,
4850
found_func = NULL;
4951
}
5052

53+
inline bool operation_attr_bool_true(TF_Operation* oper,
54+
const char* attr_name) {
55+
TF_Status* attr_status = TF_NewStatus();
56+
unsigned char value = 0;
57+
TF_OperationGetAttrBool(oper, attr_name, &value, attr_status);
58+
const bool result = TF_GetCode(attr_status) == TF_OK && value != 0;
59+
TF_DeleteStatus(attr_status);
60+
return result;
61+
}
62+
63+
inline bool read_proto_varint(const char*& ptr,
64+
const char* end,
65+
uint64_t& value) {
66+
value = 0;
67+
int shift = 0;
68+
while (ptr < end && shift <= 63) {
69+
const uint64_t byte = static_cast<unsigned char>(*ptr++);
70+
value |= (byte & 0x7f) << shift;
71+
if ((byte & 0x80) == 0) {
72+
return true;
73+
}
74+
shift += 7;
75+
}
76+
return false;
77+
}
78+
79+
inline bool read_proto_bytes(const char*& ptr,
80+
const char* end,
81+
const char*& payload,
82+
size_t& payload_size) {
83+
uint64_t size = 0;
84+
if (!read_proto_varint(ptr, end, size) ||
85+
size > static_cast<uint64_t>(end - ptr)) {
86+
return false;
87+
}
88+
payload = ptr;
89+
payload_size = static_cast<size_t>(size);
90+
ptr += payload_size;
91+
return true;
92+
}
93+
94+
inline bool skip_proto_field(const int wire_type,
95+
const char*& ptr,
96+
const char* end) {
97+
uint64_t ignored = 0;
98+
const char* payload = NULL;
99+
size_t payload_size = 0;
100+
switch (wire_type) {
101+
case 0:
102+
return read_proto_varint(ptr, end, ignored);
103+
case 1:
104+
if (end - ptr < 8) {
105+
return false;
106+
}
107+
ptr += 8;
108+
return true;
109+
case 2:
110+
return read_proto_bytes(ptr, end, payload, payload_size);
111+
case 5:
112+
if (end - ptr < 4) {
113+
return false;
114+
}
115+
ptr += 4;
116+
return true;
117+
default:
118+
return false;
119+
}
120+
}
121+
122+
inline bool proto_bytes_equal(const char* payload,
123+
const size_t payload_size,
124+
const std::string& expected) {
125+
return payload_size == expected.size() &&
126+
std::memcmp(payload, expected.data(), payload_size) == 0;
127+
}
128+
129+
inline bool attr_value_bool_true(const char* data, const size_t size) {
130+
const char* ptr = data;
131+
const char* end = data + size;
132+
while (ptr < end) {
133+
uint64_t tag = 0;
134+
if (!read_proto_varint(ptr, end, tag)) {
135+
return false;
136+
}
137+
const int field_number = static_cast<int>(tag >> 3);
138+
const int wire_type = static_cast<int>(tag & 0x7);
139+
// AttrValue.b = 5, encoded as a varint bool.
140+
if (field_number == 5 && wire_type == 0) {
141+
uint64_t value = 0;
142+
return read_proto_varint(ptr, end, value) && value != 0;
143+
}
144+
if (!skip_proto_field(wire_type, ptr, end)) {
145+
return false;
146+
}
147+
}
148+
return false;
149+
}
150+
151+
inline bool function_attr_bool_true(TF_Function* func, const char* attr_name) {
152+
TF_Status* attr_status = TF_NewStatus();
153+
TF_Buffer* attr_value = TF_NewBuffer();
154+
TF_FunctionGetAttrValueProto(func, attr_name, attr_value, attr_status);
155+
const bool result =
156+
TF_GetCode(attr_status) == TF_OK && attr_value->data != NULL &&
157+
attr_value_bool_true(static_cast<const char*>(attr_value->data),
158+
attr_value->length);
159+
TF_DeleteBuffer(attr_value);
160+
TF_DeleteStatus(attr_status);
161+
return result;
162+
}
163+
164+
inline bool attr_entry_is_xla_must_compile_true(const char* data,
165+
const size_t size) {
166+
const char* ptr = data;
167+
const char* end = data + size;
168+
bool key_matches = false;
169+
bool value_is_true = false;
170+
while (ptr < end) {
171+
uint64_t tag = 0;
172+
if (!read_proto_varint(ptr, end, tag)) {
173+
return false;
174+
}
175+
const int field_number = static_cast<int>(tag >> 3);
176+
const int wire_type = static_cast<int>(tag & 0x7);
177+
if (wire_type == 2 && (field_number == 1 || field_number == 2)) {
178+
const char* payload = NULL;
179+
size_t payload_size = 0;
180+
if (!read_proto_bytes(ptr, end, payload, payload_size)) {
181+
return false;
182+
}
183+
if (field_number == 1) {
184+
key_matches =
185+
proto_bytes_equal(payload, payload_size, "_XlaMustCompile");
186+
} else if (field_number == 2) {
187+
value_is_true = attr_value_bool_true(payload, payload_size);
188+
}
189+
} else if (!skip_proto_field(wire_type, ptr, end)) {
190+
return false;
191+
}
192+
}
193+
return key_matches && value_is_true;
194+
}
195+
196+
inline bool node_def_uses_xla(const char* data, const size_t size) {
197+
const char* ptr = data;
198+
const char* end = data + size;
199+
while (ptr < end) {
200+
uint64_t tag = 0;
201+
if (!read_proto_varint(ptr, end, tag)) {
202+
return false;
203+
}
204+
const int field_number = static_cast<int>(tag >> 3);
205+
const int wire_type = static_cast<int>(tag & 0x7);
206+
if (wire_type == 2 && (field_number == 2 || field_number == 5)) {
207+
const char* payload = NULL;
208+
size_t payload_size = 0;
209+
if (!read_proto_bytes(ptr, end, payload, payload_size)) {
210+
return false;
211+
}
212+
// NodeDef.op = 2. This identifies jax2tf native serialization.
213+
if (field_number == 2 &&
214+
proto_bytes_equal(payload, payload_size, "XlaCallModule")) {
215+
return true;
216+
}
217+
// NodeDef.attr = 5. This catches PartitionedCall nodes marked by
218+
// tf.function(jit_compile=True).
219+
if (field_number == 5 &&
220+
attr_entry_is_xla_must_compile_true(payload, payload_size)) {
221+
return true;
222+
}
223+
} else if (!skip_proto_field(wire_type, ptr, end)) {
224+
return false;
225+
}
226+
}
227+
return false;
228+
}
229+
230+
inline bool function_def_uses_xla(const char* data, const size_t size) {
231+
// TensorFlow's C API exposes TF_Function bodies only as serialized
232+
// FunctionDef protos. Use a minimal wire-format reader over the stable
233+
// FunctionDef/NodeDef/AttrValue field numbers instead of a raw byte
234+
// substring, and avoid depending on TensorFlow C++ protobuf headers.
235+
const char* ptr = data;
236+
const char* end = data + size;
237+
while (ptr < end) {
238+
uint64_t tag = 0;
239+
if (!read_proto_varint(ptr, end, tag)) {
240+
return false;
241+
}
242+
const int field_number = static_cast<int>(tag >> 3);
243+
const int wire_type = static_cast<int>(tag & 0x7);
244+
if (wire_type == 2 && (field_number == 3 || field_number == 5)) {
245+
const char* payload = NULL;
246+
size_t payload_size = 0;
247+
if (!read_proto_bytes(ptr, end, payload, payload_size)) {
248+
return false;
249+
}
250+
// FunctionDef.node_def = 3.
251+
if (field_number == 3 && node_def_uses_xla(payload, payload_size)) {
252+
return true;
253+
}
254+
// FunctionDef.attr = 5. This catches concrete functions marked by
255+
// tf.function(jit_compile=True).
256+
if (field_number == 5 &&
257+
attr_entry_is_xla_must_compile_true(payload, payload_size)) {
258+
return true;
259+
}
260+
} else if (!skip_proto_field(wire_type, ptr, end)) {
261+
return false;
262+
}
263+
}
264+
return false;
265+
}
266+
267+
inline bool function_uses_xla(TF_Function* func, TF_Status* status) {
268+
if (function_attr_bool_true(func, "_XlaMustCompile")) {
269+
return true;
270+
}
271+
TF_Buffer* func_def = TF_NewBuffer();
272+
TF_FunctionToFunctionDef(func, func_def, status);
273+
if (TF_GetCode(status) != TF_OK) {
274+
std::string msg = TF_Message(status);
275+
TF_DeleteBuffer(func_def);
276+
throw deepmd::deepmd_exception("TensorFlow C API Error: " + msg);
277+
}
278+
const bool uses_xla =
279+
func_def->data != NULL &&
280+
function_def_uses_xla(static_cast<const char*>(func_def->data),
281+
func_def->length);
282+
TF_DeleteBuffer(func_def);
283+
return uses_xla;
284+
}
285+
286+
inline bool graph_uses_xla_compilation(TF_Graph* graph) {
287+
size_t pos = 0;
288+
while (TF_Operation* oper = TF_GraphNextOperation(graph, &pos)) {
289+
const char* op_type = TF_OperationOpType(oper);
290+
if ((op_type != NULL && std::strcmp(op_type, "XlaCallModule") == 0) ||
291+
operation_attr_bool_true(oper, "_XlaMustCompile")) {
292+
return true;
293+
}
294+
}
295+
return false;
296+
}
297+
298+
inline bool uses_xla_compilation(TF_Graph* graph,
299+
const std::vector<TF_Function*>& funcs,
300+
TF_Status* status) {
301+
if (graph_uses_xla_compilation(graph)) {
302+
return true;
303+
}
304+
for (TF_Function* func : funcs) {
305+
if (function_uses_xla(func, status)) {
306+
return true;
307+
}
308+
}
309+
return false;
310+
}
311+
51312
inline TF_DataType get_data_tensor_type(const std::vector<double>& data) {
52313
return TF_DOUBLE;
53314
}
@@ -278,6 +539,7 @@ void deepmd::DeepPotJAX::init(const std::string& model,
278539
TF_Function** funcs = func_vector.data();
279540
TF_GraphGetFunctions(graph, funcs, nfuncs, status);
280541
check_status(status);
542+
uses_xla_compilation_ = uses_xla_compilation(graph, func_vector, status);
281543

282544
ctx_opts = TFE_NewContextOptions();
283545
TFE_ContextOptionsSetConfig(ctx_opts, config.data(), config.size(), status);
@@ -547,16 +809,21 @@ void deepmd::DeepPotJAX::compute(std::vector<ENERGYTYPE>& ener,
547809
std::vector<double> fparam_double(fparam.begin(), fparam.end());
548810
std::vector<double> aparam_double(aparam.begin(), aparam.end());
549811

550-
if (padding_for_nloc != nloc_real) {
551-
padding_to_nall = nall_real * PADDING_FACTOR;
552-
padding_for_nloc = nloc_real;
553-
}
554-
while (padding_to_nall < nall_real) {
555-
padding_to_nall *= PADDING_FACTOR;
812+
int nall_model = nall_real;
813+
if (uses_xla_compilation_) {
814+
if (padding_for_nloc != nloc_real) {
815+
padding_to_nall = nall_real * PADDING_FACTOR;
816+
padding_for_nloc = nloc_real;
817+
}
818+
while (padding_to_nall < nall_real) {
819+
padding_to_nall *= PADDING_FACTOR;
820+
}
821+
nall_model = padding_to_nall;
556822
}
557-
// do padding
558-
coord_double.resize(nframes * padding_to_nall * 3, 0.0);
559-
atype.resize(nframes * padding_to_nall, -1);
823+
// Padding is only useful for XLA-compiled functions; eager TF graphs can use
824+
// the exact atom count without shape recompilation churn.
825+
coord_double.resize(static_cast<size_t>(nframes) * nall_model * 3, 0.0);
826+
atype.resize(static_cast<size_t>(nframes) * nall_model, -1);
560827

561828
TFE_Op* op;
562829
if (atomic) {
@@ -569,11 +836,11 @@ void deepmd::DeepPotJAX::compute(std::vector<ENERGYTYPE>& ener,
569836
std::vector<TFE_TensorHandle*> input_list(6);
570837
std::vector<TF_Tensor*> data_tensor(6);
571838
// coord
572-
std::vector<int64_t> coord_shape = {nframes, padding_to_nall, 3};
839+
std::vector<int64_t> coord_shape = {nframes, nall_model, 3};
573840
input_list[0] =
574841
add_input(op, coord_double, coord_shape, data_tensor[0], status);
575842
// atype
576-
std::vector<int64_t> atype_shape = {nframes, padding_to_nall};
843+
std::vector<int64_t> atype_shape = {nframes, nall_model};
577844
input_list[1] = add_input(op, atype, atype_shape, data_tensor[1], status);
578845
// nlist
579846
if (ago == 0) {
@@ -600,8 +867,8 @@ void deepmd::DeepPotJAX::compute(std::vector<ENERGYTYPE>& ener,
600867
}
601868
input_list[2] = add_input(op, nlist, nlist_shape, data_tensor[2], status);
602869
// mapping; for now, set it to -1, assume it is not used
603-
std::vector<int64_t> mapping_shape = {nframes, padding_to_nall};
604-
std::vector<int64_t> mapping(nframes * padding_to_nall, -1);
870+
std::vector<int64_t> mapping_shape = {nframes, nall_model};
871+
std::vector<int64_t> mapping(static_cast<size_t>(nframes) * nall_model, -1);
605872
// pass mapping if it is given in the neighbor list
606873
if (lmp_list.mapping) {
607874
// assume nframes is 1

0 commit comments

Comments
 (0)