Skip to content
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

[Target] Add target host field for target specification #7462

Merged
merged 11 commits into from
Feb 23, 2021
15 changes: 12 additions & 3 deletions include/tvm/target/target.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ class TargetNode : public Object {
public:
/*! \brief The kind of the target device */
TargetKind kind;
/*! \brief Target host information, must be Target type */
Optional<ObjectRef> host;
zxybazh marked this conversation as resolved.
Show resolved Hide resolved
/*! \brief Tag of the the target, can be empty */
String tag;
/*! \brief Keys for this target */
Expand All @@ -64,6 +66,7 @@ class TargetNode : public Object {
v->Visit("tag", &tag);
v->Visit("keys", &keys);
v->Visit("attrs", &attrs);
v->Visit("host", &host);
}

/*!
Expand Down Expand Up @@ -122,12 +125,12 @@ class Target : public ObjectRef {
TVM_DLL explicit Target(std::nullptr_t) { data_ = nullptr; }
/*!
* \brief Construct a Target given a string
* \param tag_or_config_or_target_str the string to parse
* \param tag_or_config_or_target_str the string to parse for target
*/
TVM_DLL explicit Target(const String& tag_or_config_or_target_str);
/*!
* \brief Construct a Target using a JSON-like configuration
* \param config The JSON-like configuration
* \param config The JSON-like configuration for target
*/
TVM_DLL explicit Target(const Map<String, ObjectRef>& config);
/*!
Expand All @@ -139,7 +142,13 @@ class Target : public ObjectRef {
* allow_not_defined is true.
*/
TVM_DLL static tvm::Target Current(bool allow_not_defined = true);

/*!
* \brief Construct a Target given target and host
* \param target The Target typed object with host field undefined for target
* \param host The Target typed object for target host
* \return The Target with given target and host context information
*/
TVM_DLL explicit Target(Target target, Target host);
TVM_DEFINE_OBJECT_REF_METHODS(Target, ObjectRef, TargetNode);

private:
Expand Down
3 changes: 2 additions & 1 deletion include/tvm/target/target_kind.h
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,8 @@ inline TargetKindRegEntry& TargetKindRegEntry::set_name() {
.add_attr_option<String>("tag") \
.add_attr_option<String>("device") \
.add_attr_option<String>("model") \
.add_attr_option<Array<String>>("libs")
.add_attr_option<Array<String>>("libs") \
.add_attr_option<Target>("host")
Comment on lines +379 to +380
Copy link
Contributor

@leandron leandron Feb 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just curious on how this plays with existing logic in build() at build_module.py, which has a target_host parameter:

def build(mod, target=None, target_host=None, params=None, mod_name="default"):

Does this mean we now have two ways of declaring target_host? If so, are planning to deprecate one of them?

Also, in case the user declares both (doesn't make much sense, but can be done once this PR is meged), which one takes precedece, or, should we show an error? I'm referrin to something like relay.build(target="cuda --host nvidia/jetson-nano", target_host="llvm"...)

cc @manupa-arm

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is super important question, so I answered on the thread below: #7462 (comment). Thank you Leandro for bringing this up!


} // namespace tvm

Expand Down
16 changes: 14 additions & 2 deletions python/tvm/target/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class Target(Object):
- :py:func:`tvm.target.intel_graphics` create Intel Graphics target
"""

def __init__(self, tag_or_str_or_dict):
def __init__(self, tag_or_str_or_dict, host_tag_or_str_or_dict=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the API, I would almost expect Target(target_host, sub_target_0, sub_target_1, ...)

what would be the future path towards supporting multiple sub-targets? would there be a target_host object which contains all the sub-targets?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of sub-targets, I think it will be better to converge to composite targets instead, where we can specify a list of targets as --devices: https://github.com/apache/tvm/blob/main/src/target/target_kind.cc#L311-L313.

We can help create a short cut to construct the composite kind, like adding a static function

@staticmethod
def composite(target, devices):

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the API, I would almost expect Target(target_host, sub_target_0, sub_target_1, ...)

In terms of API, that would be very good indeed!

If internally we would convert that to represent the required composite target, with specific dictionary representing all the internal data structures, etc., that's implementation details the we care about, but the end-user, interested in compiling a model shouldn't.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@areusch as a follow-up to this, we've recently updated tvmc to accept something similar - in terms of user facing syntax to express "targets" for compilation - to what you present here.

Check it out on #7304 for code and https://discuss.tvm.apache.org/t/byoc-partitioning-support-on-tvmc/8901 for discussion.

"""Construct a TVM target object from
1) Raw target string
2) Target config dict
Expand Down Expand Up @@ -86,10 +86,22 @@ def __init__(self, tag_or_str_or_dict):
mfloat-abi : str (optional)
An llvm setting that is one of 'hard' or 'soft' indicating whether to use
hardware or software floating-point operations.
host : Union[str, Dict[str, Any]] (optional)
Description for target host. Can be recursive. Similar to tag_or_str_or_dict.
host_tag_or_str_or_dict : Optional[Union[str, Dict[str, Any]]]
Similar to tag_or_str_or_dict but for target host. Can be one of a literal
target host string, a json string describing a configuration, or a dictionary of
configuration options. When using a dictionary or json string to configure target,
the possible values are same as tag_or_str_or_dict.
"""
if not isinstance(tag_or_str_or_dict, (dict, str, Target)):
raise ValueError("target has to be a string or dictionary.")
self.__init_handle_by_constructor__(_ffi_api.Target, tag_or_str_or_dict)
if host_tag_or_str_or_dict is not None:
self.__init_handle_by_constructor__(
_ffi_api.Target, Target(tag_or_str_or_dict), Target(host_tag_or_str_or_dict)
)
else:
self.__init_handle_by_constructor__(_ffi_api.Target, tag_or_str_or_dict)

def __enter__(self):
_ffi_api.TargetEnterScope(self)
Expand Down
29 changes: 28 additions & 1 deletion src/target/target.cc
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,15 @@ Target::Target(const Map<String, ObjectRef>& config) {
data_ = std::move(target);
}

Target::Target(Target target, Target host) {
ObjectPtr<TargetNode> n = make_object<TargetNode>(*target.get());
CHECK(!n->host.defined())
<< "ValueError: Adding a host to a target whose host field has been defined";
junrushao marked this conversation as resolved.
Show resolved Hide resolved
// add target host into host field
n->host = std::move(host);
data_ = std::move(n);
}

std::vector<std::string> TargetNode::GetKeys() const {
std::vector<std::string> result;
for (auto& expr : keys) {
Expand Down Expand Up @@ -456,8 +465,18 @@ void TargetInternal::ConstructorDispatcher(TVMArgs args, TVMRetValue* rv) {
<< runtime::ArgTypeCode2Str(arg.type_code());
}
return;
} else if (args.num_args == 2) {
if (args[0].IsObjectRef<Target>() && args[1].IsObjectRef<Target>()) {
Target target = args[0];
Target host = args[1];
*rv = Target(target, host);
} else {
LOG(FATAL) << "ValueError: Invalid type of arguments. Expect 2 Target arguments.";
}
return;
}
LOG(FATAL) << "ValueError: Invalid number of arguments. Expect 1, but gets: " << args.num_args;
LOG(FATAL) << "ValueError: Invalid number of arguments. Expect 1 or 2, but gets: "
<< args.num_args;
}

ObjectPtr<Object> TargetInternal::FromString(const String& tag_or_config_or_target_str) {
Expand Down Expand Up @@ -527,6 +546,7 @@ ObjectPtr<Object> TargetInternal::FromConfig(std::unordered_map<String, ObjectRe
const String kTag = "tag";
const String kKeys = "keys";
const String kDeviceName = "device";
const String kHost = "host";
ObjectPtr<TargetNode> target = make_object<TargetNode>();
// parse 'kind'
if (config.count(kKind)) {
Expand Down Expand Up @@ -599,6 +619,13 @@ ObjectPtr<Object> TargetInternal::FromConfig(std::unordered_map<String, ObjectRe
throw dmlc::Error(": Error when parsing target[\"" + key + "\"]" + e.what());
}
}
// parse host
if (config.count(kHost)) {
target->host = PackedFunc(ConstructorDispatcher)(config[kHost]).AsObjectRef<Target>();
config.erase(kHost);
} else {
target->host = NullOpt;
}
// set default attribute values if they do not exist
for (const auto& kv : target->kind->key2default_) {
if (!attrs.count(kv.first)) {
Expand Down
1 change: 0 additions & 1 deletion src/target/target_kind.cc
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,6 @@ TVM_REGISTER_TARGET_KIND("hybrid", kDLCPU) // line break
.add_attr_option<Bool>("system-lib");

TVM_REGISTER_TARGET_KIND("composite", kDLCPU)
.add_attr_option<Target>("target_host")
.add_attr_option<Array<Target>>("devices");

/********** Registry **********/
Expand Down
66 changes: 59 additions & 7 deletions tests/python/unittest/test_target_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# under the License.
import json
import tvm
import pytest
from tvm import te
from tvm.target import cuda, rocm, mali, intel_graphics, arm_cpu, vta, bifrost, hexagon

Expand Down Expand Up @@ -113,18 +114,14 @@ def test_config_map():
attributes fails as expected.
"""
target_config = {"kind": "llvm", "libs": {"a": "b", "c": "d"}}
failed = False
try:
with pytest.raises(ValueError):
tvm.target.Target(target_config)
except ValueError:
failed = True
assert failed


def test_composite_target():
tgt = tvm.target.Target("composite --target_host=llvm --devices=cuda,opencl")
tgt = tvm.target.Target("composite --host=llvm --devices=cuda,opencl")
assert tgt.kind.name == "composite"
assert tgt.attrs["target_host"].kind.name == "llvm"
assert tgt.attrs["host"].kind.name == "llvm"
assert len(tgt.attrs["devices"]) == 2
cuda_device, opencl_device = tgt.attrs["devices"]
assert cuda_device.kind.name == "cuda"
Expand Down Expand Up @@ -158,6 +155,61 @@ def test_list_kinds():
assert all(isinstance(target_name, str) for target_name in targets)


def test_target_host_tags():
tgt = tvm.target.Target("nvidia/jetson-nano", "nvidia/geforce-rtx-2080-ti")
assert tgt.kind.name == "cuda"
assert tgt.attrs["arch"] == "sm_53"
assert tgt.attrs["shared_memory_per_block"] == 49152
assert tgt.attrs["max_threads_per_block"] == 1024
assert tgt.attrs["thread_warp_size"] == 32
assert tgt.attrs["registers_per_block"] == 32768
assert tgt.host.kind.name == "cuda"
assert tgt.host.attrs["arch"] == "sm_75"
assert tgt.host.attrs["shared_memory_per_block"] == 49152
assert tgt.host.attrs["max_threads_per_block"] == 1024
assert tgt.host.attrs["thread_warp_size"] == 32
assert tgt.host.attrs["registers_per_block"] == 65536


def test_target_host_tag_dict():
zxybazh marked this conversation as resolved.
Show resolved Hide resolved
tgt = tvm.target.Target("nvidia/jetson-nano", {"kind": "llvm"})
assert tgt.kind.name == "cuda"
assert tgt.attrs["arch"] == "sm_53"
assert tgt.attrs["shared_memory_per_block"] == 49152
assert tgt.attrs["max_threads_per_block"] == 1024
assert tgt.attrs["thread_warp_size"] == 32
assert tgt.attrs["registers_per_block"] == 32768
assert tgt.host.kind.name == "llvm"


def test_target_host_single_dict():
tgt = tvm.target.Target({"kind": "llvm", "host": "nvidia/jetson-nano"})
assert tgt.kind.name == "llvm"
assert tgt.host.kind.name == "cuda"
assert tgt.host.attrs["arch"] == "sm_53"
assert tgt.host.attrs["shared_memory_per_block"] == 49152
assert tgt.host.attrs["max_threads_per_block"] == 1024
assert tgt.host.attrs["thread_warp_size"] == 32
assert tgt.host.attrs["registers_per_block"] == 32768


def test_target_host_single_string():
tgt = tvm.target.Target("cuda --host llvm")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(putting this comment here, but this is not comment about this test, just wanted to pin it somewhere with a reference)

I noticed there are some syntax changes in this. If this becomes (with this PR) our preferred way to declare the "target to target host" dependency, should we also update the tutorials to reflect that?

assert tgt.kind.name == "cuda"
assert tgt.host.kind.name == "llvm"


def test_target_host_single_string_with_tag():
tgt = tvm.target.Target("cuda --host nvidia/jetson-nano")
assert tgt.kind.name == "cuda"
assert tgt.host.kind.name == "cuda"
assert tgt.host.attrs["arch"] == "sm_53"
assert tgt.host.attrs["shared_memory_per_block"] == 49152
assert tgt.host.attrs["max_threads_per_block"] == 1024
assert tgt.host.attrs["thread_warp_size"] == 32
assert tgt.host.attrs["registers_per_block"] == 32768


if __name__ == "__main__":
test_target_dispatch()
test_target_string_parse()
Expand Down