| #include "ulp.h" | |
| #include <cstring> | |
| #include "caffe2/operators/conv_pool_op_base.h" | |
| #include "caffe2/utils/eigen_utils.h" | |
| #include "ulp_neon.h" | |
| namespace caffe2 { | |
| void uniformQuantize2b1b(const TensorCPU& X, | |
| const std::vector<std::unique_ptr<TensorCPU>>& XQ, | |
| float offset, | |
| float inter_center_distance) { | |
| CAFFE_ENFORCE_GT(X.ndim(), 1); | |
| const auto N = X.size_to_dim(X.ndim() - 1); | |
| auto C = X.size() / N; | |
| const auto QC = divRoundUp(C, 8); | |
| auto XQs = X.sizes().vec(); | |
| XQs[X.ndim() - 1] = QC; | |
| CAFFE_ENFORCE_EQ(XQ.size(), k2b1bXBits); | |
| for (auto i = 0; i < k2b1bXBits; ++i) { | |
| XQ[i]->Resize(XQs); | |
| } | |
| const float* Xdata = X.data<float>(); | |
| std::array<uint8_t*, k2b1bXBits> XQdata; | |
| for (auto i = 0; i < k2b1bXBits; ++i) { | |
| XQdata[i] = XQ[i]->mutable_data<uint8_t>(); | |
| } | |
| for (auto n = 0; n < N; ++n) { | |
| for (auto qc = 0; qc < QC; ++qc) { | |
| // compute the block in X. | |
| std::array<uint8_t, k2b1bXBits> p = {{0, 0}}; | |
| for (auto b = 0; b < 8; ++b) { | |
| const auto c = qc * 8 + b; | |
| if (c < C) { | |
| float v = Xdata[qc * 8 + b + C * n]; | |
| if (v < offset) { | |
| // zero'd already. | |
| } else if (v < offset + inter_center_distance) { | |
| p[0] |= 1 << b; | |
| } else if (v < offset + 2 * inter_center_distance) { | |
| p[1] |= 1 << b; | |
| } else { | |
| p[0] |= 1 << b; | |
| p[1] |= 1 << b; | |
| } | |
| } | |
| } | |
| for (auto i = 0; i < k2b1bXBits; ++i) { | |
| XQdata[i][qc + QC * n] = p[i]; | |
| } | |
| } | |
| } | |
| } | |
| void qconv(const ConvArgs& args, | |
| const TensorCPU& X, | |
| const TensorCPU& W, | |
| const TensorCPU* b, | |
| TensorCPU* Y) { | |
| const auto N = X.dim32(0); | |
| const auto IH = X.dim32(1); | |
| const auto IW = X.dim32(2); | |
| const auto KH = W.dim32(1); | |
| const auto KW = W.dim32(2); | |
| const auto KC = W.dim32(3); | |
| Y->Resize(X.dim32(0), | |
| (X.dim32(1) - KH + args.pad_t + args.pad_b) / args.stride_h + 1, | |
| (X.dim32(2) - KW + args.pad_l + args.pad_r) / args.stride_w + 1, | |
| W.dim32(0)); | |
| const auto OH = Y->dim32(1); | |
| const auto OW = Y->dim32(2); | |
| const auto OC = Y->dim32(3); | |
| CAFFE_ENFORCE_EQ(W.dim32(3), X.dim32(3)); | |
| const auto* Xdata = X.data<uint8_t>(); | |
| const auto* Wdata = W.data<uint8_t>(); | |
| auto* Ydata = Y->mutable_data<float>(); | |
| for (size_t n = 0; n < N; ++n) { | |
| for (size_t oh = 0; oh < OH; ++oh) { | |
| for (size_t ow = 0; ow < OW; ++ow) { | |
| for (size_t oc = 0; oc < OC; ++oc) { | |
| float acc = 0.0; | |
| for (size_t kh = 0; kh < KH; ++kh) { | |
| const int32_t ih = (int32_t)kh + (int32_t)args.stride_h * oh - (int32_t)args.pad_t; | |
| for (size_t kw = 0; kw < KW; ++kw) { | |
| const int32_t iw = (int32_t)kw + (int32_t)args.stride_w * ow - (int32_t)args.pad_l; | |
| for (size_t kc = 0; kc < KC; ++kc) { | |
| const uint8_t w = Wdata[kc + KC * kw + KC * KW * kh + KC * KW * KH * oc]; | |
| // Use unsigned integer math to avoid multiple comparisons (>= H, < 0). | |
| if ((size_t)ih >= (size_t)IH || (size_t)iw >= (size_t)IW) { | |
| acc += __builtin_popcount(0 ^ w); | |
| } else { | |
| const uint8_t x = | |
| Xdata[kc + KC * (size_t)iw + KC * IW * (size_t)ih + n * KC * IW * IH]; | |
| const uint8_t w = Wdata[kc + KC * kw + KC * KW * kh + KC * KW * KH * oc]; | |
| acc += __builtin_popcount(x ^ w); | |
| } | |
| } | |
| } | |
| } | |
| Ydata[oc + OC * ow + OC * OW * oh + n * OC * OW * OH] = | |
| KW * KH * KC * 8 - 2 * acc + (b ? b->data<float>()[oc] : 0.0); | |
| ; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| void qpad_zero(const ConvArgs& args, const TensorCPU& X, TensorCPU* Y) { | |
| CAFFE_ENFORCE_EQ(args.stride_h, 1); | |
| CAFFE_ENFORCE_EQ(args.stride_w, 1); | |
| const auto* Xdata = X.data<uint8_t>(); | |
| Y->Resize(X.dim32(0), | |
| X.dim32(1) + args.pad_t + args.pad_b, | |
| X.dim32(2) + args.pad_l + args.pad_r, | |
| X.dim32(3)); | |
| auto* Ydata = Y->mutable_data<uint8_t>(); | |
| ::memset(Ydata, 0, Y->nbytes()); | |
| const auto C = Y->dim32(3); | |
| const auto XrowSize = X.dim32(3) * X.dim32(2); | |
| const auto YrowSize = Y->dim32(3) * Y->dim32(2); | |
| math::CopyMatrix<CPUContext>(1, | |
| X.dim32(1), | |
| XrowSize, | |
| Xdata, | |
| XrowSize, | |
| Ydata + C * args.pad_l + YrowSize * args.pad_t, | |
| YrowSize, | |
| nullptr); | |
| } | |
| void signQuantize(const TensorCPU& X, TensorCPU* XQ) { | |
| CAFFE_ENFORCE_GT(X.ndim(), 1); | |
| const auto N = X.size_to_dim(X.ndim() - 1); | |
| auto C = X.size() / N; | |
| const auto QC = divRoundUp(C, 8); | |
| auto XQs = X.sizes().vec(); | |
| XQs[X.ndim() - 1] = QC; | |
| XQ->Resize(XQs); | |
| const float* Xdata = X.data<float>(); | |
| uint8_t* XQdata = XQ->mutable_data<uint8_t>(); | |
| for (auto n = 0; n < N; ++n) { | |
| for (auto qc = 0; qc < QC; ++qc) { | |
| // compute the block in X. | |
| uint8_t p = 0; | |
| for (auto b = 0; b < 8; ++b) { | |
| const auto c = qc * 8 + b; | |
| if (c < C) { | |
| p |= (Xdata[c + C * n] > 0) << b; | |
| } | |
| } | |
| XQdata[qc + QC * n] = p; | |
| } | |
| } | |
| } | |
| void filterNormalization11(const TensorCPU& WQ, TensorCPU* WQN) { | |
| const auto F = WQ.dim32(0); | |
| // In our NEON kernel we read up to TileSize, so align allocation to TileSize elements. | |
| WQN->Resize(divRoundUp(F, kGEMMTileSize) * kGEMMTileSize); | |
| const auto WQs = WQ.size() / F; | |
| const auto WQbits = 8 * WQs; | |
| const auto* WQdata = WQ.data<uint8_t>(); | |
| auto* WQNdata = WQN->mutable_data<float>(); | |
| for (auto f = 0; f < F; ++f) { | |
| int32_t bitSum = 0; | |
| for (auto j = 0; j < WQs; ++j) { | |
| bitSum += __builtin_popcount(WQdata[f * WQs + j]); | |
| } | |
| TORCH_DCHECK_LE(bitSum, WQbits); | |
| WQNdata[f] = 2 * bitSum - WQbits; | |
| } | |
| } | |
| void filterNormalizationL1(const TensorCPU& W, TensorCPU* WL1) { | |
| const auto F = W.dim32(0); | |
| WL1->Resize(F); | |
| const auto Ws = W.size() / F; | |
| const auto* Wdata = W.data<float>(); | |
| auto* WL1data = WL1->mutable_data<float>(); | |
| for (auto f = 0; f < F; ++f) { | |
| double l1sum = 0.0; | |
| for (auto j = 0; j < Ws; ++j) { | |
| l1sum += std::abs(Wdata[f * Ws + j]); | |
| } | |
| WL1data[f] = l1sum / Ws; | |
| } | |
| } | |
| void qim2col(const ConvArgs& args, const TensorCPU& XQ, const TensorCPU& WQ, TensorCPU* XQcol) { | |
| // TODO: pass pre-resized output? | |
| // TODO: handle strides? | |
| CAFFE_ENFORCE_EQ(XQ.dim32(3), WQ.dim32(3)); | |
| const size_t N = XQ.dim32(0); | |
| const size_t IH = XQ.dim32(1); | |
| const size_t IW = XQ.dim32(2); | |
| const size_t KH = WQ.dim32(1); | |
| const size_t KW = WQ.dim32(2); | |
| const size_t KC = WQ.dim32(3); | |
| XQcol->Resize(XQ.dim32(0), | |
| (XQ.dim32(1) - KH + args.pad_t + args.pad_b) / args.stride_h + 1, | |
| (XQ.dim32(2) - KW + args.pad_l + args.pad_r) / args.stride_w + 1, | |
| KH * KW * KC); | |
| if (args.pad_l == 0 && args.pad_r == 0 && args.pad_b == 0 && args.pad_t == 0 && | |
| args.stride_h == 1 && args.stride_w == 1 && KH == 1 && KW == 1) { | |
| CAFFE_ENFORCE_EQ(XQ.size(), XQcol->size()); | |
| XQcol->ShareExternalPointer(const_cast<uint8_t*>(XQ.data<uint8_t>()), XQ.size()); | |
| return; | |
| } | |
| const size_t OH = XQcol->dim32(1); | |
| const size_t OW = XQcol->dim32(2); | |
| const uint8_t* XQdata = XQ.data<uint8_t>(); | |
| uint8_t* XQcoldata = XQcol->mutable_data<uint8_t>(); | |
| for (size_t n = 0; n < N; ++n) { | |
| for (size_t oh = 0; oh < OH; ++oh) { | |
| int32_t h_pad = (int32_t)(args.stride_h * oh) - (int32_t)args.pad_t; | |
| for (size_t ow = 0; ow < OW; ++ow) { | |
| int32_t w_pad = (int32_t)(args.stride_w * ow) - (int32_t)args.pad_l; | |
| for (size_t kh = 0; kh < KH; ++kh) { | |
| int32_t ih = (int32_t)kh + h_pad; | |
| if ((size_t)ih < (size_t)IH && (size_t)w_pad < (size_t)IW && | |
| (size_t)((int32_t)w_pad + (int32_t)KW) < (size_t)IW) { | |
| // We can do a larger memcpy, of size KW * KC | |
| size_t off = kh * KW * KC + ow * KH * KW * KC + oh * KH * KW * KC * OW + | |
| n * KH * KW * KC * OW * OH; | |
| std::memcpy(&XQcoldata[off], | |
| &XQdata[((int32_t)w_pad) * KC + ih * IW * KC + n * IW * KC * IH], | |
| KW * KC); | |
| } else { | |
| for (size_t kw = 0; kw < KW; ++kw) { | |
| int32_t iw = (int32_t)kw + w_pad; | |
| // Use unsigned integer math to avoid multiple comparisons (>= H, < 0). | |
| size_t off = kw * KC + kh * KW * KC + ow * KH * KW * KC + oh * KH * KW * KC * OW + | |
| n * KH * KW * KC * OW * OH; | |
| if ((size_t)ih < (size_t)IH && (size_t)iw < (size_t)IW) { | |
| std::memcpy( | |
| &XQcoldata[off], &XQdata[iw * KC + ih * IW * KC + n * KC * IW * IH], KC); | |
| } else { | |
| // This should be simply padded with zero. | |
| std::memset(&XQcoldata[off], 0, KC); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| std::unique_ptr<QConvState> create2b1bConvState(Workspace* ws, | |
| const TensorCPU& W, | |
| const TensorCPU* b) { | |
| auto state = std::make_unique<QConvState>(); | |
| state->XQs.resize(k2b1bXBits); | |
| state->YQs.resize(k2b1bXBits); | |
| for (auto i = 0; i < k2b1bXBits; ++i) { | |
| state->XQs[i] = std::make_unique<Tensor>(CPU); | |
| state->YQs[i] = std::make_unique<Tensor>(CPU); | |
| } | |
| state->WQ = std::make_unique<Tensor>(CPU); | |
| state->WQN = std::make_unique<Tensor>(CPU); | |
| state->WQL1Norm = std::make_unique<Tensor>(CPU); | |
| state->scratch = std::make_unique<Tensor>(CPU); | |
| state->scratchColBuffer = std::make_unique<Tensor>(CPU); | |
| signQuantize(W, state->WQ.get()); | |
| filterNormalization11(*(state->WQ), state->WQN.get()); | |
| filterNormalizationL1(W, state->WQL1Norm.get()); | |
| // TODO: incorporate center distance normalization. | |
| // Since inputs to convs are [0, 1, 2, 3], instead of [0, x, 2 * x, ...], | |
| // we can just uniformly rescale the outputs by x, i.e., | |
| // for (auto i = 0; i < r->WQL1Norm.size(); ++i) { | |
| // r->WQL1Norm.mutable_data<float>()[i] *= center_distance; | |
| // } | |
| state->parallelFor = [ws](size_t range, std::function<void(size_t)> f) { | |
| #ifdef C10_MOBILE | |
| ws->GetThreadPool()->run([&](int, size_t v) { f(v); }, range); | |
| #else | |
| for (size_t v = 0; v < range; ++v) { | |
| f(v); | |
| } | |
| #endif | |
| }; | |
| if (b) { | |
| state->bias = std::make_unique<Tensor>(*b, CPU); | |
| } | |
| return state; | |
| } | |
| void run2b1bConvGeneric(QConvState* state, const ConvArgs& args, const TensorCPU& X, TensorCPU* Y) { | |
| #if defined(__ARM_NEON__) || defined(__ARM_NEON) | |
| if (run2b1bConvNeon(state, args, X, Y)) { | |
| return; | |
| } | |
| #endif | |
| uniformQuantize2b1b(X, state->XQs, 0.5, 1.0); | |
| for (auto i = 0; i < k2b1bXBits; ++i) { | |
| qconv(args, *(state->XQs[i]), *(state->WQ), nullptr, state->YQs[i].get()); | |
| } | |
| Y->ResizeLike(*(state->YQs[0])); | |
| const auto F = state->WQ->dim(0); | |
| const auto N = Y->size() / F; | |
| run2b1bUnification(state, | |
| N, | |
| F, | |
| state->WQN->data<float>(), | |
| state->YQs[0]->data<float>(), | |
| state->YQs[1]->data<float>(), | |
| F, | |
| Y->mutable_data<float>(), | |
| F, | |
| state->bias ? state->bias->data<float>() : nullptr); | |
| } | |
| void run2b1bUnification(QConvState* state, | |
| size_t N, | |
| size_t C, | |
| const float* WQNVdata, | |
| const float* YQs0Vdata, | |
| const float* YQs1Vdata, | |
| size_t YQstride, | |
| float* Ydata, | |
| size_t Ystride, | |
| const float* bias) { | |
| ConstEigenVectorArrayMap<float> WQNV(WQNVdata, C); | |
| for (size_t j = 0; j < N; ++j) { | |
| ConstEigenVectorArrayMap<float> YQs0V(YQs0Vdata + YQstride * j, C); | |
| ConstEigenVectorArrayMap<float> YQs1V(YQs1Vdata + YQstride * j, C); | |
| EigenVectorArrayMap<float> YNV(Ydata + Ystride * j, C); | |
| if (bias) { | |
| ConstEigenVectorArrayMap<float> BV(bias, C); | |
| YNV = (std::pow<float>(2, k2b1bXBits) - 1) / 2 * WQNV + std::pow<float>(2, -1) * YQs0V + | |
| std::pow<float>(2, 0) * YQs1V + BV; | |
| } else { | |
| YNV = (std::pow<float>(2, k2b1bXBits) - 1) / 2 * WQNV + std::pow<float>(2, -1) * YQs0V + | |
| std::pow<float>(2, 0) * YQs1V; | |
| } | |
| } | |
| } | |
| class QConvOp final : public ConvPoolOpBase<CPUContext> { | |
| public: | |
| QConvOp(const OperatorDef& operator_def, Workspace* ws) | |
| : ConvPoolOpBase<CPUContext>(operator_def, ws), ws_(ws) { | |
| OPERATOR_NEEDS_FEATURE(this->order_ == StorageOrder::NHWC, "QConvOp only supports NHWC order"); | |
| OPERATOR_NEEDS_FEATURE(this->dilation_h() == 1, ""); | |
| OPERATOR_NEEDS_FEATURE(this->dilation_w() == 1, ""); | |
| OPERATOR_NEEDS_FEATURE(this->group_ == 1, ""); | |
| } | |
| bool RunOnDeviceWithOrderNHWC() override { | |
| auto& X = Input(0); | |
| auto& filter = Input(1); | |
| const auto* bias = InputSize() == 3 ? &Input(2) : nullptr; | |
| auto* Y = Output(0); | |
| // TODO: Support multiple quantization methods instead of assuming 2b1b. | |
| if (!state_) { | |
| state_ = create2b1bConvState(ws_, filter, bias); | |
| } | |
| ConvArgs args; | |
| args.pad_l = this->pad_l(); | |
| args.pad_t = this->pad_t(); | |
| args.pad_b = this->pad_b(); | |
| args.pad_r = this->pad_r(); | |
| args.stride_h = this->stride_h(); | |
| args.stride_w = this->stride_w(); | |
| run2b1bConvGeneric(state_.get(), args, X, Y); | |
| return true; | |
| } | |
| private: | |
| std::unique_ptr<QConvState> state_; | |
| Workspace* ws_; | |
| }; | |
| REGISTER_CPU_OPERATOR(QConv, QConvOp); | |
| } // namespace caffe2 |