Skip to content

Commit 88af985

Browse files
committed
Enable CustomOp with floats (python list) as attributes
Summary: !ci_branch_mk2 Users can pass INTs as python list into custom op attributes from python interface. But floats are not allowed to be passed into attributes directly. Floats will be converted into INT64 and may cause casting issue. Solution: Using py::isinstance to determine which types this python list is. Then use different branch to create int vector or float vector respectively. The vector then be added into the dictionary. This solution can be used to solve the issue described in the slack thread. There is also a pytest file added. This diff is to solve this issue. https://phabricator.sourcevertex.net/T74394 Reviewers: #popart, alexhu, #framework_ip_review_-_any_oss_or_third-party_code_use_has_been_approved, matthewha, dariuszs Reviewed By: #popart, #framework_ip_review_-_any_oss_or_third-party_code_use_has_been_approved, dariuszs Subscribers: dariuszs, grahamh, zihangl Maniphest Tasks: T74394 Differential Revision: https://phabricator.sourcevertex.net/D82661
1 parent e380caf commit 88af985

File tree

5 files changed

+311
-7
lines changed

5 files changed

+311
-7
lines changed

python/popart/popart_core/popart.cpp

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,15 @@ getOptimizerValueDictionary(py::dict e) {
129129
return cpm;
130130
}
131131

132+
template <typename T> std::vector<T> handlePythonList(py::list pylist) {
133+
std::vector<T> vec(pylist.size());
134+
std::transform(pylist.begin(),
135+
pylist.end(),
136+
vec.begin(),
137+
[](const py::handle &h) { return h.cast<T>(); });
138+
return vec;
139+
}
140+
132141
std::map<std::string, popart::any> getDictionaryVar(py::dict pydict) {
133142
// This attempts to convert the py::dict to a map of string, popart::any.
134143
// Since we do not know the python types given by the user until runtime, we
@@ -141,20 +150,33 @@ std::map<std::string, popart::any> getDictionaryVar(py::dict pydict) {
141150
auto val = element.second;
142151
if (py::isinstance<py::str>(val)) {
143152
// String
144-
dictionary.insert(std::make_pair(key, val.cast<std::string>()));
153+
dictionary.emplace(key, val.cast<std::string>());
145154
} else if (py::isinstance<py::int_>(val)) {
146155
// Int
147-
dictionary.insert(std::make_pair(key, val.cast<int64_t>()));
156+
dictionary.emplace(key, val.cast<std::int64_t>());
148157
} else if (py::isinstance<py::list>(val)) {
158+
py::list py_list = val.cast<py::list>();
149159
// Ints
150-
std::vector<int64_t> vec;
151-
for (auto subval : val) {
152-
vec.push_back(subval.cast<int64_t>());
160+
if (std::all_of(val.begin(), val.end(), [](py::handle item) {
161+
return py::isinstance<py::int_>(item);
162+
})) {
163+
std::vector<int64_t> vec = handlePythonList<int64_t>(py_list);
164+
dictionary.emplace(key, vec);
165+
}
166+
// Floats
167+
else if (std::all_of(val.begin(), val.end(), [](py::handle item) {
168+
return py::isinstance<py::float_>(item);
169+
})) {
170+
std::vector<float> vec = handlePythonList<float>(py_list);
171+
dictionary.emplace(key, vec);
172+
} else {
173+
throw error("Invalid or mismatched python list type provided in custom "
174+
"op attribute '{}'",
175+
key);
153176
}
154-
dictionary.insert(std::make_pair(key, vec));
155177
} else if (py::isinstance<py::float_>(val)) {
156178
// Float
157-
dictionary.insert(std::make_pair(key, val.cast<float>()));
179+
dictionary.emplace(key, val.cast<float>());
158180
} else {
159181
throw error("Invalid type provided in custom op attribute '{}'", key);
160182
}

tests/integration/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,4 @@ add_subdirectory(transformation_tests)
196196
add_subdirectory(slice_pad_dual_tests)
197197
add_subdirectory(unwinding_tests)
198198
add_subdirectory(verify_cxx_11_interface)
199+
add_subdirectory(customop_attr_pylist_tests)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Copyright (c) 2023 Graphcore Ltd. All rights reserved.
2+
add_library(attr_pylist_float_customOp SHARED attr_pylist_float_customOp.cpp)
3+
4+
target_link_libraries(attr_pylist_float_customOp
5+
popart-internal
6+
)
7+
8+
install(TARGETS attr_pylist_float_customOp DESTINATION ${INSTALL_TESTS}/integration/customop_attr_pylist_tests)
9+
10+
add_popart_py_unit_test(customop_attr_pylist_float_test)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) 2023 Graphcore Ltd. All rights reserved.
2+
3+
#include <vector>
4+
// #include <filereader.hpp>
5+
// #include <testdevice.hpp>
6+
7+
#include "popart/datatype.hpp"
8+
#include <popart/op.hpp>
9+
#include <popart/opmanager.hpp>
10+
#include <popart/popx/opx.hpp>
11+
#include <popart/popx/opxmanager.hpp>
12+
13+
namespace popart {
14+
namespace popx {
15+
class Devicex;
16+
} // namespace popx
17+
} // namespace popart
18+
19+
namespace poplar {
20+
namespace program {
21+
class Sequence;
22+
} // namespace program
23+
} // namespace poplar
24+
25+
using namespace popart;
26+
27+
namespace CustomOperators {
28+
const OperatorIdentifier CustomopAttrPylist = {"com.acme",
29+
"CustomopAttrPylist",
30+
1};
31+
} // namespace CustomOperators
32+
33+
// An IdentityOp that doesn't return any grad ops.
34+
class CustomopAttrPylistOp : public Op {
35+
public:
36+
CustomopAttrPylistOp(const OperatorIdentifier &_opid,
37+
const Op::Settings &settings_)
38+
: Op(_opid, settings_) {}
39+
40+
void setup() final { outInfo(0) = inInfo(0); }
41+
42+
std::unique_ptr<Op> clone() const final {
43+
return std::make_unique<CustomopAttrPylistOp>(*this);
44+
}
45+
46+
float getSubgraphValue() const final { return getLowSubgraphValue(); }
47+
};
48+
49+
static popart::OpDefinition CustomopAttrPylistOpDef(
50+
{popart::OpDefinition::Inputs({
51+
{"input", {{popart::DataType::FLOAT, popart::DataType::FLOAT16}}},
52+
}),
53+
popart::OpDefinition::Outputs(
54+
{{"output", {{popart::DataType::FLOAT, popart::DataType::FLOAT16}}}}),
55+
popart::OpDefinition::Attributes({{"values", {"*"}}})});
56+
57+
static OpCreator<CustomopAttrPylistOp> CustomopAttrPylistOpCreator(
58+
OpDefinitions({{CustomOperators::CustomopAttrPylist,
59+
CustomopAttrPylistOpDef}}),
60+
[](const OpCreatorInfo &info) -> std::unique_ptr<Op> {
61+
const OperatorIdentifier &opid = info.opid;
62+
const Op::Settings &settings = info.settings;
63+
const Attributes &attr = info.attributes;
64+
auto values = attr.getAttribute<Attributes::Floats>("values");
65+
return std::unique_ptr<Op>(new CustomopAttrPylistOp(opid, settings));
66+
},
67+
true);
68+
69+
class CustomopAttrPylistOpx : public popx::Opx {
70+
public:
71+
CustomopAttrPylistOpx(Op *op, popx::Devicex *devicex)
72+
: popx::Opx(op, devicex) {
73+
verifyOp<CustomopAttrPylistOp>(op, CustomOperators::CustomopAttrPylist);
74+
}
75+
76+
void grow(poplar::program::Sequence &prog) const final {
77+
insert(outId(0), cloneNcopy(prog, getInTensor(0)));
78+
}
79+
};
80+
81+
static popx::OpxCreator<CustomopAttrPylistOpx>
82+
CustomopAttrPylistOpxCreator(CustomOperators::CustomopAttrPylist);
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# Copyright (c) 2023 Graphcore Ltd. All rights reserved.
2+
import numpy as np
3+
import popart
4+
import sys
5+
6+
# `import test_util` requires adding to sys.path
7+
from pathlib import Path
8+
9+
sys.path.append(str(Path(__file__).resolve().parent.parent))
10+
import test_util as tu
11+
12+
import ctypes
13+
14+
ctypes.cdll.LoadLibrary("./libattr_pylist_float_customOp.so")
15+
16+
17+
def test_customOp_python_floats():
18+
builder = popart.Builder()
19+
20+
input_tensor = builder.addInputTensor(popart.TensorInfo("FLOAT", [1]))
21+
22+
output_tensor = builder.customOp(
23+
opName="CustomopAttrPylist",
24+
opVersion=1,
25+
inputs=[input_tensor],
26+
domain="com.acme",
27+
attributes={"values": [1.0, 2.0, 3.0]},
28+
)[0]
29+
30+
builder.addOutputTensor(output_tensor)
31+
proto = builder.getModelProto()
32+
33+
# Create a runtime environment
34+
anchors = {output_tensor: popart.AnchorReturnType("All")}
35+
dataFlow = popart.DataFlow(1, anchors)
36+
37+
with tu.create_test_device() as device:
38+
session = popart.InferenceSession(proto, dataFlow, device)
39+
40+
session.prepareDevice()
41+
42+
anchors = session.initAnchorArrays()
43+
44+
input = np.random.rand(1).astype(np.float32)
45+
46+
stepio = popart.PyStepIO({input_tensor: input}, anchors)
47+
48+
session.run(stepio)
49+
assert np.allclose(input, anchors["CustomopAttrPylist:0"])
50+
51+
52+
def test_customOp_python_ints():
53+
builder = popart.Builder()
54+
55+
input_tensor = builder.addInputTensor(popart.TensorInfo("FLOAT", [1]))
56+
57+
output_tensor = builder.customOp(
58+
opName="CustomopAttrPylist",
59+
opVersion=1,
60+
inputs=[input_tensor],
61+
domain="com.acme",
62+
attributes={"values": [1, 2, 3]},
63+
)[0]
64+
65+
builder.addOutputTensor(output_tensor)
66+
proto = builder.getModelProto()
67+
68+
# Create a runtime environment
69+
anchors = {output_tensor: popart.AnchorReturnType("All")}
70+
dataFlow = popart.DataFlow(1, anchors)
71+
72+
with tu.create_test_device() as device:
73+
session = popart.InferenceSession(proto, dataFlow, device)
74+
75+
session.prepareDevice()
76+
77+
anchors = session.initAnchorArrays()
78+
79+
input = np.random.rand(1).astype(np.float32)
80+
81+
stepio = popart.PyStepIO({input_tensor: input}, anchors)
82+
83+
session.run(stepio)
84+
assert np.allclose(input, anchors["CustomopAttrPylist:0"])
85+
86+
87+
def test_customOp_python_string():
88+
builder = popart.Builder()
89+
90+
input_tensor = builder.addInputTensor(popart.TensorInfo("FLOAT", [1]))
91+
92+
output_tensor = builder.customOp(
93+
opName="CustomopAttrPylist",
94+
opVersion=1,
95+
inputs=[input_tensor],
96+
domain="com.acme",
97+
attributes={"values": "Foo_Bar"},
98+
)[0]
99+
100+
builder.addOutputTensor(output_tensor)
101+
proto = builder.getModelProto()
102+
103+
# Create a runtime environment
104+
anchors = {output_tensor: popart.AnchorReturnType("All")}
105+
dataFlow = popart.DataFlow(1, anchors)
106+
107+
with tu.create_test_device() as device:
108+
session = popart.InferenceSession(proto, dataFlow, device)
109+
110+
session.prepareDevice()
111+
112+
anchors = session.initAnchorArrays()
113+
114+
input = np.random.rand(1).astype(np.float32)
115+
116+
stepio = popart.PyStepIO({input_tensor: input}, anchors)
117+
118+
session.run(stepio)
119+
assert np.allclose(input, anchors["CustomopAttrPylist:0"])
120+
121+
122+
def test_customOp_python_int():
123+
builder = popart.Builder()
124+
125+
input_tensor = builder.addInputTensor(popart.TensorInfo("FLOAT", [1]))
126+
127+
output_tensor = builder.customOp(
128+
opName="CustomopAttrPylist",
129+
opVersion=1,
130+
inputs=[input_tensor],
131+
domain="com.acme",
132+
attributes={"values": 10},
133+
)[0]
134+
135+
builder.addOutputTensor(output_tensor)
136+
proto = builder.getModelProto()
137+
138+
# Create a runtime environment
139+
anchors = {output_tensor: popart.AnchorReturnType("All")}
140+
dataFlow = popart.DataFlow(1, anchors)
141+
142+
with tu.create_test_device() as device:
143+
session = popart.InferenceSession(proto, dataFlow, device)
144+
145+
session.prepareDevice()
146+
147+
anchors = session.initAnchorArrays()
148+
149+
input = np.random.rand(1).astype(np.float32)
150+
151+
stepio = popart.PyStepIO({input_tensor: input}, anchors)
152+
153+
session.run(stepio)
154+
assert np.allclose(input, anchors["CustomopAttrPylist:0"])
155+
156+
157+
def test_customOp_python_float():
158+
builder = popart.Builder()
159+
160+
input_tensor = builder.addInputTensor(popart.TensorInfo("FLOAT", [1]))
161+
162+
output_tensor = builder.customOp(
163+
opName="CustomopAttrPylist",
164+
opVersion=1,
165+
inputs=[input_tensor],
166+
domain="com.acme",
167+
attributes={"values": 10.0},
168+
)[0]
169+
170+
builder.addOutputTensor(output_tensor)
171+
proto = builder.getModelProto()
172+
173+
# Create a runtime environment
174+
anchors = {output_tensor: popart.AnchorReturnType("All")}
175+
dataFlow = popart.DataFlow(1, anchors)
176+
177+
with tu.create_test_device() as device:
178+
session = popart.InferenceSession(proto, dataFlow, device)
179+
180+
session.prepareDevice()
181+
182+
anchors = session.initAnchorArrays()
183+
184+
input = np.random.rand(1).astype(np.float32)
185+
186+
stepio = popart.PyStepIO({input_tensor: input}, anchors)
187+
188+
session.run(stepio)
189+
assert np.allclose(input, anchors["CustomopAttrPylist:0"])

0 commit comments

Comments
 (0)