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

[Partitioner] Heterogeneous partition testing #3197

Closed
wants to merge 1 commit into from

Conversation

beicy
Copy link
Contributor

@beicy beicy commented Jun 28, 2019

Summary:
Provide an easier way for using heterogeneous partition : user can define which ops not running on which type of backends.
related to #2814
Documentation:

[Optional Fixes #issue]

Test Plan:
added unittest;

wangm-mbp:buildR wangm$ ./bin/image-classifier tests/images/imagenet/cat_285.png -image-mode=0to1 -m=resnet50 -model-input-name=gpu_0/data -cpu-memory=100000 -load-device-configs="tests/runtime_test/heterogeneousConfigs.yaml" 
Model: resnet50
Running 1 thread(s).
tests/runtime_test/heterogeneousConfigs.yaml
WARNING: Logging before InitGoogleLogging() is written to STDERR
I0628 16:12:13.164857 43896832 Partitioner.cpp:1038] The number of partitions is : 3, and the DAG is dumped into DAG.dot file.
I0628 16:12:13.165649 43896832 Partitioner.cpp:58] Writing dotty graph for DAG after graph partitioning: DAG.dot
I0628 16:12:13.165989 43896832 Partitioner.cpp:1047] 	 Partition 0:
		 Name :	resnet50_part1_part1
		 BackendKind :	CPU
		 Memory :	94929408
		 LogicalDeviceIDs :	1
I0628 16:12:13.166002 43896832 Partitioner.cpp:1047] 	 Partition 1:
		 Name :	resnet50_part2_part1
		 BackendKind :	Interpreter
		 Memory :	409600
		 LogicalDeviceIDs :	2
I0628 16:12:13.166026 43896832 Partitioner.cpp:1047] 	 Partition 2:
		 Name :	resnet50_part3_part1
		 BackendKind :	CPU
		 Memory :	8208200
		 LogicalDeviceIDs :	0
 File: tests/images/imagenet/cat_285.png	Label-K1: 285 (probability: 0.5823)

Please see a detailed explanation of how to fill out the fields in the relevant sections in PULL_REQUEST.md.

@opti-mix
Copy link
Contributor

@beicy Nice! Could you also add an option to specify the opposite?

The following should mean that only AvgPool is supported on the specific backend and everything else should be considered not supported by the backend.

parameters: |
    "supportedOp":"AvgPool"

I hope adding this options should be trivial.

/// supported by this DeviceManger. It doesn't mean that those nodes kind are
/// not supported by this device. It just means user don't want these kind of
/// nodes running on this device.
llvm::StringRef getNonSupprtedNodes() const {
Copy link
Contributor

Choose a reason for hiding this comment

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

It is probably better to implement getParamByName(llvm::StringRef name) to make it more reusable. And then you can just call it like getParamByName("nonSupportedOp")

std::set<Kinded::Kind> generateNonSupportedNodes(llvm::StringRef names) {
std::set<Kinded::Kind> ret;
while (names.find(',') != std::string::npos) {
std::pair<llvm::StringRef, llvm::StringRef> newSplit = names.split(',');
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably not very important, but could it be that split allocates new strings? Please have a look at the similar function for splitting comma-separated input here:

https://llvm.org/doxygen/CommandLine_8cpp_source.html#l00607

Copy link
Contributor

Choose a reason for hiding this comment

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

/me wishes for folly::split (but of course we don't want to pull in folly, alas)

@beicy beicy force-pushed the rafatory branch 2 times, most recently from 5920f3d to a5eacc1 Compare July 1, 2019 23:17
Copy link
Contributor

@opti-mix opti-mix left a comment

Choose a reason for hiding this comment

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

Thanks for adding supportedNodes!

Please address a couple of minor comments below and the PR should be good to merge!

// Step 3: Check if the node is actually supported in this backend, if so,
// assign it to this backend and now we find the first backend that
// supports this node (The order of backends is important) and break.
// Otherwise goto Step 4.
Copy link
Contributor

Choose a reason for hiding this comment

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

This Step 3 sounds odd. I mean this part:
if so, assign it to this backend and now we find the first backend that supports this node. Could you rephrase?

// Step 1:
auto nonSupported =
backendMap_[backend->getBackendName()].nonSupportedNodesKind;
if (nonSupported.find(N.getKind()) != nonSupported.end()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Small nit: The following could be a bit shorter:

if (nonSupported.count(N.getKind()) {
  continue;
}

@@ -38,6 +38,10 @@ struct BackendInfo {
uint64_t memSize;
/// Backend pointer.
Backend *backend = nullptr;
/// The non-supported nodes kind.
std::set<Kinded::Kind> nonSupportedNodesKind;
Copy link
Contributor

Choose a reason for hiding this comment

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

You store multiple kinds in the set, right? So, should it be called nonSupportedNodesKinds? Same for supportedNodesKinds?

// assigned to the next backend.

// Step 1:
auto nonSupported =
Copy link
Contributor

Choose a reason for hiding this comment

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

auto nonSupportedNodesKinds?

continue;
}
// Step 2:
auto supported =
Copy link
Contributor

Choose a reason for hiding this comment

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

supportedNodesKinds?

// Step 2:
auto supported =
backendMap_[backend->getBackendName()].supportedNodesKind;
if (supported.size() > 0 &&
Copy link
Contributor

Choose a reason for hiding this comment

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

How about a simpler form?

if (!supprted.empty() && supported.count(N.getKind())) {
  // Continue.
}

@@ -286,4 +286,16 @@ GraphMemInfo getGraphMemInfo(const NodesSetTy &nodes) {
return ret;
}

std::set<Kinded::Kind> generateNodeKindsSet(llvm::StringRef names) {
std::set<Kinded::Kind> ret;
Copy link
Contributor

Choose a reason for hiding this comment

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

ret -> nodeKindsSet?

- name: Device1
backendName: CPU
parameters: |
"nonSupportedNodes":"AvgPool"
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add supportedNodes example as well, because it needs to be tested?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The "Device3" in this file has the "supportedNodes" params. Since there are many ops in resnet50, I just let Interpreter supports all. In unit test, we tested the supported nodes set.

@@ -38,6 +38,10 @@ struct BackendInfo {
uint64_t memSize;
/// Backend pointer.
Backend *backend = nullptr;
/// The non-supported nodes kind.
std::set<Kinded::Kind> nonSupportedNodesKind;
Copy link
Contributor

Choose a reason for hiding this comment

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

Unless order matters here, prefer llvm::DenseSet or maybe std::unordered_set.

@@ -51,6 +51,14 @@ struct DeviceInfo {
uint64_t availableMemory;
/// Backend Type.
std::string backendName;
/// A string contains the node names(e.g. Add, Div) which are seperated by
Copy link
Contributor

Choose a reason for hiding this comment

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

sp: separated

// is important.
// is important. The check flow is :

// Step 1: If a node is in pre-defined non-supported nodes set, it can not
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it's just my preference but here I would find it easier to read the "step N" comments inline with the code, rather than as a separate block that I need to scroll up to reference. And that way we could get rid of the "step N" tags themselves, which have to be kept up to date over time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

// assigned to the next backend.

// Step 1:
auto nonSupported =
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably want const auto &nonSupported = ... here, or this is going to invoke copy assignment.

auto nonSupported =
backendMap_[backend->getBackendName()].nonSupportedNodesKind;
if (nonSupported.find(N.getKind()) != nonSupported.end()) {
// This op is on the pre-definded non-supported op list:
Copy link
Contributor

Choose a reason for hiding this comment

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

sp: pre-defined

std::set<Kinded::Kind> generateNonSupportedNodes(llvm::StringRef names) {
std::set<Kinded::Kind> ret;
while (names.find(',') != std::string::npos) {
std::pair<llvm::StringRef, llvm::StringRef> newSplit = names.split(',');
Copy link
Contributor

Choose a reason for hiding this comment

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

/me wishes for folly::split (but of course we don't want to pull in folly, alas)

@beicy beicy force-pushed the rafatory branch 4 times, most recently from efab6ca to ba8aa97 Compare July 2, 2019 07:41
Copy link
Contributor

@opti-mix opti-mix left a comment

Choose a reason for hiding this comment

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

The implementation part is fine now! Thanks!

All of my comments this time are about tests.

// heterogeneous partiton.
ASSERT_EQ(node->logicalDevices.size(), 1);
}
createSimpleModule(mod_);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure what exactly you test here. You set up the backends, but you do not expect anything specific from partitions. Or do you?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Those backends are mock backends which are subclass of "Interpreter" or "CPU". They overload the isOpsupported() function to eliminate some nodes for Heterogeneous testing. Previously, we didn't have such "nonSupportedNodesKinds" nor "supportedNodesKinds" config. Therefore, we need to write mock backends for unittest test.

CompilationContext cctx;
auto err = partitioner.Partition(cctx);
EXPECT_FALSE(errToBool(std::move(err)));
DAGListTy myList = std::move(partitioner.getPartitionResult());
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you give this variable a more meaningful name than myList? ;-)

CompilationContext cctx;
auto err = partitioner.Partition(cctx);
EXPECT_FALSE(errToBool(std::move(err)));
DAGListTy myList = std::move(partitioner.getPartitionResult());
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you give this variable a more meaningful name than myList? ;-)

}

/// Test pre-defined non-supported ops used for choosing backend in
/// Heterogeneous Partition.
Copy link
Contributor

Choose a reason for hiding this comment

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

It is not obvious to me from the code and comments below how the non-supported ops are tested.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The config yaml file is only loaded in "loader" flow (I added resnet50 test in tests/images/run.sh to test loading and parsing the yaml file). For Partitioner unnitest, we just pass the pre-defined nodes list to DeviceInfo used in Partitioner. Added comments in the tests.


/// Test pre-defined non-supported ops used for choosing backend in
/// Heterogeneous Partition.
TEST_F(PartitionerTest, ConfigHeterogeneousPartitioning1) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you give this test a more meaningful name reflecting what it does?


/// Test pre-defined supported ops used for choosing backend in
/// Heterogeneous Partition.
TEST_F(PartitionerTest, ConfigHeterogeneousPartitioning2) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you give this test a more meaningful name reflecting what it does?

}

/// Test pre-defined supported ops used for choosing backend in
/// Heterogeneous Partition.
Copy link
Contributor

Choose a reason for hiding this comment

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

It is not obvious to me from the code and comments below how the supported ops are tested.

CompilationContext cctx;
auto err = partitioner.Partition(cctx);
EXPECT_FALSE(errToBool(std::move(err)));
DAGListTy myList = std::move(partitioner.getPartitionResult());
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you give this variable a more meaningful name than myList? ;-)

@beicy beicy force-pushed the rafatory branch 2 times, most recently from 06a9659 to d7f0189 Compare July 2, 2019 20:25
@@ -981,6 +1003,10 @@ void Partitioner::getBackendMap(
// is the same.
// TODO : will improve the algorithm for different memory size.
backendInfo.memSize = deviceInfo_[i].availableMemory;
backendInfo.nonSupportedNodesKinds =
generateNodeKindsSet(deviceInfo_[i].nonSupportedNodes);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe std::move(generateNodeKindsSet(deviceInfo_[i].nonSupportedNodes)) to avoid copying the set?

ASSERT_EQ(dagList.size(), 1);
ASSERT_TRUE(checkSaveNode(mod_));

int numOfInterpreter = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

The names sounds odd. Maybe numOfInterpreterNodes and numOfCPUNode?

Could you also check that it is exactly the kinds of nodes (or the nodes) you expected to be on CPU and Interpreter respectively?

ASSERT_EQ(mod_.getFunctions().size(), 3);
ASSERT_EQ(dagList.size(), 1);
ASSERT_TRUE(checkSaveNode(mod_));
int numOfInterpreter = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment here about the names.

Copy link
Contributor

@opti-mix opti-mix left a comment

Choose a reason for hiding this comment

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

Looking good!

:shipit:

Copy link

@facebook-github-bot facebook-github-bot left a comment

Choose a reason for hiding this comment

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

@beicy has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@facebook-github-bot
Copy link

@beicy merged this pull request in dd135c9.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants