pytorch/caffe2/operators/tile_op.cc
Yinghai Lu eb3bf96f95 During inbatch broadcast, move Tile op after Fused8BitRowwiseQuantizedToFloat if applicable (#41464)
Summary:
Pull Request resolved: https://github.com/pytorch/pytorch/pull/41464

If input is int8 rowwise quantized, currently we cannot low it to Glow. And previously, we had some error when running with inbatch broadcast. The main issue is that Tile op doesn't support uint8_t type, which is very easily added here. However, this will result in non-ideal situation that we will leave Tile -> Fused8BitRowwiseQuantizedToFloat on host side, which probably hurt the memory bw a lot. Even we later add the support to Fused8BitRowwiseQuantizedToFloat in Glow, it's still not ideal because we are doing redudant compute on identical columns. So the solution here is to swap the order of Fused8BitRowwiseQuantizedToFloat and Tile to make it Tile -> Fused8BitRowwiseQuantizedToFloat. In this way, it will resolve the error we saw immediately. For the short term, we can still run Tile in card. And for longer term, things runs faster on card.

The optimization is a heuristic. If in the net, there isn't such pattern, inbatch broadcast will work as it was before.

(Note: this ignores all push blocking failures!)

Test Plan:
```
buck test caffe2/caffe2/opt/custom:in_batch_broadcast_test
```

Reviewed By: benjibc

Differential Revision: D22544162

fbshipit-source-id: b6dd36a5925a9c8103b80f034e7730a7a085a6ff
2020-07-16 21:25:18 -07:00

208 lines
6.0 KiB
C++

#include "caffe2/operators/tile_op.h"
#include <string>
namespace caffe2 {
template <>
bool TileOp<CPUContext>::RunOnDevice() {
return DispatchHelper<TensorTypes<
at::Half,
std::uint8_t,
std::int32_t,
std::int64_t,
float,
double,
std::string>>::call(this, Input(0));
}
template <>
template <>
bool TileOp<CPUContext>::DoRunWithType<std::string>() {
if (InputSize() > 1) {
// We potentially have tiles and/or axis specified as inputs
// as well. We will check for them in that order. In other words:
// InputSize() == 2: tiles is specified
// InputSize() == 3: tiles is specified and axis.
// Anything specified as input will override the arguments
CAFFE_ENFORCE(
Input(1).dim() == 1 && Input(1).numel() == 1,
"Input `tiles` should be a vector of size 1.");
tiles_ = GetArgFromTensor(Input(1));
// Because of a bug in original code, temporarily adds this part to keep
// backward compatibility.
// TODO(yangxm): Remove this part when prod runtime upgraded with fixed
// model config.
if (Input(1).IsType<std::int64_t>()) {
axis_ = 0;
}
if (InputSize() > 2) {
CAFFE_ENFORCE(
Input(2).dim() == 1 && Input(2).numel() == 1,
"Input `axis` should be a vector of size 1.");
axis_ = GetArgFromTensor(Input(2));
} else {
CAFFE_ENFORCE(
OperatorBase::HasArgument("axis"),
"Argument `axis` is missing and was not specified as input.");
}
} else {
CAFFE_ENFORCE(
OperatorBase::HasArgument("tiles"),
"Argument `tiles` is missing and was not specified as input.");
CAFFE_ENFORCE(
OperatorBase::HasArgument("axis"),
"Argument `axis` is missing and was not specified as input.");
}
const auto& X = Input(0);
auto* Y = Output(0);
const int axis = X.canonical_axis_index(axis_);
// reshape output to be input tiled along the axis
std::vector<std::int64_t> Y_dims = X.sizes().vec();
Y_dims[axis] *= tiles_;
Y->Resize(Y_dims);
// size up to (and not including) axis
const int outer_size = X.size_to_dim(axis);
// size from axis up
const int inner_size = X.size_from_dim(axis);
const TypeMeta& meta = X.dtype();
const int item_size = X.itemsize();
const char* X_ptr = reinterpret_cast<const char*>(X.raw_data());
char* Y_ptr = reinterpret_cast<char*>(Y->raw_mutable_data(meta));
for (int i = 0; i < outer_size; ++i) {
for (int t = 0; t < tiles_; ++t) {
context_.CopyItemsSameDevice(meta, inner_size, X_ptr, Y_ptr);
Y_ptr += inner_size * item_size;
}
X_ptr += inner_size * item_size;
}
return true;
}
REGISTER_CPU_OPERATOR(Tile, TileOp<CPUContext>);
REGISTER_CPU_OPERATOR(TileGradient, TileGradientOp<CPUContext>);
OPERATOR_SCHEMA(Tile)
.NumInputs(1, 3)
.NumOutputs(1)
.TensorInferenceFunction([](const OperatorDef& def,
const std::vector<TensorShape>& in) {
std::vector<TensorShape> out(1);
out[0] = TensorShape(in[0]);
ArgumentHelper helper(def);
const std::int32_t tiles =
helper.GetSingleArgument<std::int32_t>("tiles", 1);
const std::int32_t axis =
helper.GetSingleArgument<std::int32_t>("axis", 0);
if (in.size() > 1) {
// Tile or axis is specified as input; we can't determine
// the size
out[0].set_unknown_shape(true);
} else {
const auto canonical_axis =
canonical_axis_index_(axis, out[0].dims().size());
out[0].set_dims(
canonical_axis, out[0].dims().Get(canonical_axis) * tiles);
}
return out;
})
.SetDoc(R"DOC(
Constructs a tensor by tiling a given tensor along a specified axis. This operation creates a new tensor by replicating the input tensor a number of times specified by the `tiles` argument along the `axis` dimension. The output tensor's `axis` dimension has $(X.dims(axis) * tiles)$ elements.
Github Links:
- https://github.com/pytorch/pytorch/blob/master/caffe2/operators/tile_op.cc
<details>
<summary> <b>Example</b> </summary>
**Code**
```
workspace.ResetWorkspace()
op = core.CreateOperator(
"Tile",
["X", "tiles", "axis"],
["Y"]
)
workspace.FeedBlob("X", np.random.randint(10, size=(5,5)))
workspace.FeedBlob("tiles", np.array([5]).astype(np.int32))
workspace.FeedBlob("axis", np.array([1]).astype(np.int32))
print("X:", workspace.FetchBlob("X"))
workspace.RunOperatorOnce(op)
print("Y:", workspace.FetchBlob("Y"))
```
**Result**
```
X:
[[9 1 7 1 3]
[2 3 6 2 5]
[0 9 2 6 4]
[5 8 1 5 9]
[2 0 1 3 7]]
Y:
[[9 1 7 1 3 9 1 7 1 3 9 1 7 1 3 9 1 7 1 3 9 1 7 1 3]
[2 3 6 2 5 2 3 6 2 5 2 3 6 2 5 2 3 6 2 5 2 3 6 2 5]
[0 9 2 6 4 0 9 2 6 4 0 9 2 6 4 0 9 2 6 4 0 9 2 6 4]
[5 8 1 5 9 5 8 1 5 9 5 8 1 5 9 5 8 1 5 9 5 8 1 5 9]
[2 0 1 3 7 2 0 1 3 7 2 0 1 3 7 2 0 1 3 7 2 0 1 3 7]]
```
</details>
)DOC")
.Arg("tiles", "(*int*): number of replicas")
.Arg("axis", "(*int*): axis to replicate along")
.Input(0, "X", "(*Tensor*): input tensor")
.Input(
1,
"tiles",
"(*Tensor`<int>`*): [OPTIONAL] number of replicas (overrides `tiles` argument)")
.Input(
2,
"axis",
"(*Tensor`<int>`*): [OPTIONAL] axis to replicate along (overrides `axis` argument)")
.Output(0, "Y", "(*Tensor*): output tensor")
.InheritOnnxSchema();
OPERATOR_SCHEMA(TileGradient).NumInputs(1, 3).NumOutputs(1);
namespace {
class GetTileGradient : public GradientMakerBase {
using GradientMakerBase::GradientMakerBase;
std::vector<OperatorDef> GetGradientDefs() override {
// Check whether the tiles/axis information was
// passed through input arguments
std::vector<std::string> g_inputs({GO(0)});
if (Def().input_size() > 1) {
g_inputs.push_back(I(1));
}
if (Def().input_size() > 2) {
g_inputs.push_back(I(2));
}
return SingleGradientDef(
"TileGradient", "", g_inputs, std::vector<std::string>{GI(0)});
}
};
} // namespace
REGISTER_GRADIENT(Tile, GetTileGradient);
} // namespace caffe2