1 /**
2 * Copyright (c) Glow Contributors. See CONTRIBUTORS file.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include "BackendTestUtils.h"
18
19 #include "glow/ExecutionEngine/ExecutionEngine.h"
20 #include "glow/Graph/Graph.h"
21 #include "glow/IR/IR.h"
22 #include "glow/Optimizer/GraphOptimizer/GraphOptimizer.h"
23 #include "glow/Quantization/Base/Base.h"
24 #include "glow/Quantization/Base/Calibration.h"
25 #include "glow/Quantization/Base/Profile.h"
26 #include "glow/Quantization/Quantization.h"
27 #include "glow/Quantization/Serialization.h"
28
29 #include "gtest/gtest.h"
30
31 #include "llvm/ADT/SmallVector.h"
32 #include "llvm/Support/FileSystem.h"
33
34 namespace glow {
35
36 using llvm::cast;
37
38 class Quantization : public ::testing::TestWithParam<std::string> {};
39
40 class Operator
41 : public ::testing::TestWithParam<::std::tuple<std::string, std::string>> {
42 protected:
43 ExecutionEngine profileEE{};
44 ExecutionEngine backendSpecificEE{};
45
SetUp()46 void SetUp() override {
47 std::string backend1;
48 std::string backend2;
49 std::tie(backend1, backend2) = GetParam();
50 profileEE.setBackendName(backend1);
51 backendSpecificEE.setBackendName(backend2);
52 }
53 };
54
55 class InterpAndCPU : public Operator {};
56
operator ==(const std::vector<float> & lhs,const std::vector<float> & rhs)57 bool operator==(const std::vector<float> &lhs, const std::vector<float> &rhs) {
58 return std::equal(lhs.begin(), lhs.end(), rhs.begin());
59 }
60
operator ==(const NodeProfilingInfo & lhs,const NodeProfilingInfo & rhs)61 bool operator==(const NodeProfilingInfo &lhs, const NodeProfilingInfo &rhs) {
62 return lhs.min() == rhs.min() && lhs.max() == rhs.max() &&
63 lhs.nodeOutputName_ == rhs.nodeOutputName_ &&
64 lhs.histogram() == rhs.histogram();
65 }
66
operator ==(const NodeQuantizationInfo & lhs,const NodeQuantizationInfo & rhs)67 bool operator==(const NodeQuantizationInfo &lhs,
68 const NodeQuantizationInfo &rhs) {
69 return lhs.scale() == rhs.scale() && lhs.offset() == rhs.offset() &&
70 lhs.nodeOutputName_ == rhs.nodeOutputName_;
71 }
72
73 /// This is a mock backend which extended support of quantized operators.
74 class MockQuantBackend : public Backend {
75 // The actual backend being wrapped.
76 std::unique_ptr<Backend> backend_;
77
78 public:
MockQuantBackend()79 MockQuantBackend() { backend_.reset(createBackend("Interpreter")); }
80
getBackendName() const81 std::string getBackendName() const override { return "Interpreter"; }
82
83 Expected<std::unique_ptr<CompiledFunction>>
compile(Function * F,const BackendOptions & opts) const84 compile(Function *F, const BackendOptions &opts) const override {
85 return backend_->compile(F, opts);
86 }
87
88 runtime::DeviceManager *
createDeviceManager(const runtime::DeviceConfig & deviceConfig)89 createDeviceManager(const runtime::DeviceConfig &deviceConfig) override {
90 return nullptr;
91 }
92
isOpSupported(const NodeInfo & NI) const93 bool isOpSupported(const NodeInfo &NI) const override {
94 if (NI.getKind() == Kinded::Kind::SoftMaxNodeKind ||
95 NI.getKind() == Kinded::Kind::LocalResponseNormalizationNodeKind ||
96 NI.getKind() == Kinded::Kind::SaveNodeKind ||
97 NI.getKind() == Kinded::Kind::ReluNodeKind ||
98 NI.getKind() == Kinded::Kind::SelectNodeKind ||
99 NI.getKind() == Kinded::Kind::LogNodeKind ||
100 NI.getKind() == Kinded::Kind::SigmoidNodeKind ||
101 NI.getKind() == Kinded::Kind::TanhNodeKind) {
102 return true;
103 }
104 return backend_->isOpSupported(NI);
105 }
106 };
107
108 /// Simple tests to verify the histogram rescale.
TEST(Quantization,rescaleHistogramTest)109 TEST(Quantization, rescaleHistogramTest) {
110 EXPECT_EQ(quantization::rescaleHistogram({}, 0.0f, 1.0f, 0.0f, 2.0).size(),
111 0);
112 EXPECT_EQ(
113 quantization::rescaleHistogram({1, 2, 3, 4}, 0.0f, 1.0f, -1.0f, 1.0),
114 std::vector<float>({0, 0, 3, 7}));
115 EXPECT_EQ(
116 quantization::rescaleHistogram({2, 4, 6, 8}, -1.0f, 1.0f, 0.0f, 1.0),
117 std::vector<float>({3, 3, 4, 4}));
118 }
119
120 /// Simple tests to verify the KL optimization.
TEST(Quantization,optimizeKLTest)121 TEST(Quantization, optimizeKLTest) {
122 // Test that an all-zero histogram does not raise exceptions.
123 std::vector<float> histAllZero(1000, 0);
124 quantization::FloatRange rangeAllZero =
125 quantization::optimizeKL(histAllZero, 0.f, 1.0f, 255);
126 EXPECT_EQ(rangeAllZero.first, 0.f);
127 EXPECT_EQ(rangeAllZero.second, 1.0f);
128
129 // Test that an empty histogram does not raise exceptions.
130 std::vector<float> histEmpty;
131 quantization::FloatRange rangeEmpty =
132 quantization::optimizeKL(histEmpty, 0.f, 1.0f, 255);
133 EXPECT_EQ(rangeEmpty.first, 0.f);
134 EXPECT_EQ(rangeEmpty.second, 1.0f);
135 }
136
testProfilingInfosSerialization(std::vector<NodeProfilingInfo> & expected)137 void testProfilingInfosSerialization(std::vector<NodeProfilingInfo> &expected) {
138 llvm::SmallVector<char, 10> resultPath;
139 llvm::sys::fs::createTemporaryFile("prefix", "suffix", resultPath);
140 std::string filePath(resultPath.begin(), resultPath.end());
141 llvm::hash_code hash = 13;
142 serializeProfilingInfosToYaml(filePath, hash, expected);
143 std::vector<NodeProfilingInfo> deserialized;
144 llvm::hash_code hashDeserialized;
145 deserializeProfilingInfosFromYaml(filePath, hashDeserialized, deserialized);
146 llvm::sys::fs::remove(filePath);
147 EXPECT_EQ(static_cast<size_t>(hash), static_cast<size_t>(hashDeserialized));
148 EXPECT_EQ(expected, deserialized);
149 }
150
TEST(Quantization,ProfilingSerialize)151 TEST(Quantization, ProfilingSerialize) {
152 std::vector<float> histEmpty;
153 std::vector<float> hist = {0, 1, 2, 3, 4};
154 std::vector<NodeProfilingInfo> expected{{"first", {1.0, 10.0, histEmpty}},
155 {"second", {-1.0, 3.0, hist}},
156 {"third", {-10.0, 30.0, hist}},
157 {"fourth", {0.1, 10.0, hist}},
158 {"fifth", {0.123, 30.0, hist}}};
159 testProfilingInfosSerialization(expected);
160 }
161
TEST(Quantization,ProfilingSerializePower2Range)162 TEST(Quantization, ProfilingSerializePower2Range) {
163 std::vector<NodeProfilingInfo> expected{
164 {"pwr_0", {1.0000000000f, 1.0f}}, {"pwr_1", {0.5000000000f, 2.0f}},
165 {"pwr_2", {0.2500000000f, 4.0f}}, {"pwr_3", {0.1250000000f, 8.0f}},
166 {"pwr_4", {0.0625000000f, 16.0f}}, {"pwr_5", {0.0312500000f, 32.0f}},
167 {"pwr_6", {0.0156250000f, 64.0f}}, {"pwr_7", {0.0078125000f, 128.0f}},
168 {"pwr_8", {0.0039062500f, 256.0f}}, {"pwr_9", {0.0019531250f, 512.0f}}};
169 testProfilingInfosSerialization(expected);
170 }
171
172 #if LLVM_VERSION_MAJOR < 8
TEST(Quantization,ProfilingSerializeEmpty)173 TEST(Quantization, ProfilingSerializeEmpty) {
174 std::vector<NodeProfilingInfo> expected;
175 testProfilingInfosSerialization(expected);
176 }
177 #endif
178
TEST(Quantization,tensorAverageValue)179 TEST(Quantization, tensorAverageValue) {
180 {
181 float min = -10.0;
182 float max = 10.0;
183 std::vector<float> hist = {64, 64};
184 TensorProfilingParams profParams(min, max, hist);
185 float avgVal = quantization::getTensorAverageValue(profParams);
186 EXPECT_FLOAT_EQ(avgVal, 0.0);
187 }
188 {
189 float min = -10.0;
190 float max = 10.0;
191 std::vector<float> hist = {0, 64};
192 TensorProfilingParams profParams(min, max, hist);
193 float avgVal = quantization::getTensorAverageValue(profParams);
194 EXPECT_FLOAT_EQ(avgVal, 5.0);
195 }
196 {
197 float min = 0.0;
198 float max = 10.0;
199 std::vector<float> hist = {64, 0};
200 TensorProfilingParams profParams(min, max, hist);
201 float avgVal = quantization::getTensorAverageValue(profParams);
202 EXPECT_FLOAT_EQ(avgVal, 2.5);
203 }
204 }
205
clip(From in)206 template <typename From, typename To> static To clip(From in) {
207 static_assert(sizeof(From) >= sizeof(To),
208 "Clip should reduce the variable size");
209 auto mx = std::numeric_limits<To>::max();
210 auto mn = std::numeric_limits<To>::min();
211 return std::max<From>(mn, std::min<From>(mx, in));
212 }
213
TEST(Quantization,quantScaleOffset)214 TEST(Quantization, quantScaleOffset) {
215 // Test different scale values from 1<<-23 to 1>>1.
216 float scales[] = {
217 0.0000001596f, 0.00000025f, 0.000000995f, 0.0000035f, 0.00000952f,
218 0.00000113f, 0.000721f, 0.0000721f, 0.0000172f, 0.0000951f,
219 0.0000721f, 0.0000341f, 0.0000222f, 0.0000172f, 0.000752f,
220 0.000371f, 0.000321f, 0.000223f, 0.000112f, 0.00852f,
221 0.00671f, 0.00592f, 0.00200f, 0.00107f, 0.0931f,
222 0.0721f, 0.031f, 0.014f, 0.0132f, 0.712f,
223 0.613f, 0.412f, 0.223f, 0.134f, 1.0f,
224 1.13f, 1.612f, 1.523f, 2.0f};
225
226 // Try all scale factors:
227 for (float scale : scales) {
228 // Try all legal integers within the range:
229 for (int8_t input = -128; input < 127; input++) {
230 int32_t sum32num = round(input / scale);
231
232 auto TR = quantization::quantizeScaleOffset32To8(scale, 0);
233 int32_t computed = TR.transform(sum32num);
234
235 EXPECT_NEAR(input, computed, 1);
236 }
237 }
238 }
239
TEST(Quantization,quantScaleOffsetPower2Scale)240 TEST(Quantization, quantScaleOffsetPower2Scale) {
241 // Test different power of 2 scale values (from 2^-10 to 2^1).
242 float scales[] = {0.0009765625f, 0.0019531250f, 0.0039062500f, 0.0078125000f,
243 0.0156250000f, 0.0312500000f, 0.0625000000f, 0.1250000000f,
244 0.2500000000f, 0.5000000000f, 1.0000000000f, 2.0000000000f};
245
246 // Try all scale factors:
247 for (float scale : scales) {
248 // Try all legal integers within the range:
249 for (int8_t input = -128; input < 127; input++) {
250 int32_t sum32num = round(input / scale);
251 auto TR = quantization::quantizeScaleOffset32To8(scale, 0);
252 EXPECT_EQ(quantization::isFloatPowerOf2(scale), true);
253 EXPECT_EQ(TR.pre, 0);
254 int exp = quantization::getFloat2Exp(scale);
255 if (exp > 0) {
256 EXPECT_EQ(TR.scale, (int)scale);
257 EXPECT_EQ(TR.post, 0);
258 } else {
259 EXPECT_EQ(TR.scale, 1);
260 EXPECT_EQ(TR.post, -exp);
261 }
262 int32_t computed = TR.transform(sum32num);
263 EXPECT_NEAR(input, computed, 1);
264 }
265 }
266 }
267
268 template <class qtype>
quantizeTensorTest(ElemKind qTy,quantization::Schema schema)269 void quantizeTensorTest(ElemKind qTy, quantization::Schema schema) {
270 // Map float [0.0; 6.0] to a quantized type using its entire value range.
271 TensorQuantizationParams quantParams =
272 chooseQuantizationParams({0.0, 6.0}, schema, qTy);
273
274 // Create an FP32 tensor with 6 elements and initialize it with numbers from 0
275 // to 5.
276 Tensor inputFP32(ElemKind::FloatTy, {6});
277 Handle<float> THFP32 = inputFP32.getHandle<float>();
278 for (unsigned i = 0; i < 6; ++i) {
279 THFP32.at({i}) = i * 1.0f;
280 }
281
282 // Quantize the tensor.
283 auto quantizedFP32 =
284 quantization::quantizeTensor(inputFP32, quantParams, qTy);
285 // Check that the dequantized result is close to the original values before
286 // the quantization.
287 Handle<qtype> THquantizedFP32 = quantizedFP32.getHandle<qtype>();
288 for (unsigned i = 0; i < 6; ++i) {
289 EXPECT_NEAR(THFP32.at({i}),
290 quantization::dequantize(THquantizedFP32.at({i}), quantParams),
291 0.05f);
292 }
293
294 // Create an FP16 tensor with 6 elements and initialize it with numbers from 0
295 // to 5.
296 Tensor inputFP16(ElemKind::Float16Ty, {6});
297 Handle<float16> THFP16 = inputFP16.getHandle<float16>();
298 for (unsigned i = 0; i < 6; ++i) {
299 THFP16.at({i}) = i * 1.0f;
300 }
301
302 // Quantize the tensor.
303 auto quantizedFP16 =
304 quantization::quantizeTensor(inputFP16, quantParams, qTy);
305 // Check that the dequantized result is close to the original values before
306 // the quantization.
307 Handle<qtype> THquantizedFP16 = quantizedFP16.getHandle<qtype>();
308 for (unsigned i = 0; i < 6; ++i) {
309 EXPECT_NEAR(THFP16.at({i}),
310 quantization::dequantize(THquantizedFP16.at({i}), quantParams),
311 0.05f);
312 }
313 }
314
TEST(Quantization,quantizeTensorAsymmetricInt8)315 TEST(Quantization, quantizeTensorAsymmetricInt8) {
316 quantizeTensorTest<int8_t>(ElemKind::Int8QTy,
317 quantization::Schema::Asymmetric);
318 }
TEST(Quantization,quantizeTensorAsymmetricInt16)319 TEST(Quantization, quantizeTensorAsymmetricInt16) {
320 quantizeTensorTest<int16_t>(ElemKind::Int16QTy,
321 quantization::Schema::Asymmetric);
322 }
TEST(Quantization,quantizeTensorAsymmetricInt32)323 TEST(Quantization, quantizeTensorAsymmetricInt32) {
324 quantizeTensorTest<int32_t>(ElemKind::Int32QTy,
325 quantization::Schema::Asymmetric);
326 }
TEST(Quantization,quantizeTensorSymmetricInt8)327 TEST(Quantization, quantizeTensorSymmetricInt8) {
328 quantizeTensorTest<int8_t>(ElemKind::Int8QTy,
329 quantization::Schema::Symmetric);
330 }
TEST(Quantization,quantizeTensorSymmetricInt16)331 TEST(Quantization, quantizeTensorSymmetricInt16) {
332 quantizeTensorTest<int16_t>(ElemKind::Int16QTy,
333 quantization::Schema::Symmetric);
334 }
TEST(Quantization,quantizeTensorSymmetricInt32)335 TEST(Quantization, quantizeTensorSymmetricInt32) {
336 quantizeTensorTest<int32_t>(ElemKind::Int32QTy,
337 quantization::Schema::Symmetric);
338 }
TEST(Quantization,quantizeTensorSymmetricUInt8)339 TEST(Quantization, quantizeTensorSymmetricUInt8) {
340 quantizeTensorTest<int8_t>(ElemKind::Int8QTy,
341 quantization::Schema::SymmetricWithUnsigned);
342 }
TEST(Quantization,quantizeTensorSymmetricUInt16)343 TEST(Quantization, quantizeTensorSymmetricUInt16) {
344 quantizeTensorTest<int16_t>(ElemKind::Int16QTy,
345 quantization::Schema::SymmetricWithUnsigned);
346 }
TEST(Quantization,quantizeTensorSymmetricUInt32)347 TEST(Quantization, quantizeTensorSymmetricUInt32) {
348 quantizeTensorTest<int32_t>(ElemKind::Int32QTy,
349 quantization::Schema::SymmetricWithUnsigned);
350 }
TEST(Quantization,quantizeTensorSymmetricPwr2Int8)351 TEST(Quantization, quantizeTensorSymmetricPwr2Int8) {
352 quantizeTensorTest<int8_t>(ElemKind::Int8QTy,
353 quantization::Schema::SymmetricWithPower2Scale);
354 }
TEST(Quantization,quantizeTensorSymmetricPwr2Int16)355 TEST(Quantization, quantizeTensorSymmetricPwr2Int16) {
356 quantizeTensorTest<int16_t>(ElemKind::Int16QTy,
357 quantization::Schema::SymmetricWithPower2Scale);
358 }
TEST(Quantization,quantizeTensorSymmetricPwr2Int32)359 TEST(Quantization, quantizeTensorSymmetricPwr2Int32) {
360 quantizeTensorTest<int32_t>(ElemKind::Int32QTy,
361 quantization::Schema::SymmetricWithPower2Scale);
362 }
363
364 /// Test 4-bit fused rowwise quantization.
TEST(Quantization,fused4BitsRowwiseQuantizeTensor)365 TEST(Quantization, fused4BitsRowwiseQuantizeTensor) {
366 // Create an FP32 tensor with 12 elements and initialize it
367 // with numbers from the following test inputs here.
368 // 1. Input that contains at least one +ve, one -ve and zero.
369 // 2. Input that contains at least one +ve and zero.
370 // 3. Input that contains at least one -ve and zero.
371 // 4. Input that contains at least only (+ve) numbers.
372 // 5. Input that contains at least only (-ve) numbers.
373 // 'deltas' is used to create the above 5 test cases hermetically.
374 auto deltas = {-3, 0, 3, -7, 7};
375 for (const auto &delta : deltas) {
376 Tensor inputFP32(ElemKind::FloatTy, {2, 6});
377 Tensor dequantized(ElemKind::FloatTy, {2, 6});
378 Tensor quantized(ElemKind::UInt4FusedFP16QTy, {2, 7}, /* dummy scale */ 1.0,
379 /* dummy offset */ 0);
380 Handle<float> inputH = inputFP32.getHandle<float>();
381 for (dim_t i = 0; i < 2; i++) {
382 for (dim_t j = 0; j < 6; j++) {
383 inputH.at({i, j}) = (i + j) * 1.0f + delta;
384 }
385 }
386
387 quantization::tensorFusedRowwiseQuantization<float16_t>(inputFP32,
388 quantized);
389 dequantized =
390 quantization::tensor4BitsFusedRowwiseDequantization(quantized);
391
392 Handle<float> dequantizedH = dequantized.getHandle<float>();
393 for (dim_t i = 0; i < 2; i++) {
394 for (dim_t j = 0; j < 6; j++) {
395 EXPECT_NEAR(inputH.at({i, j}), dequantizedH.at({i, j}), 0.02f);
396 }
397 }
398 }
399 }
400
401 /// When quantizing a scalar the quantization should not lose precision: the
402 /// quantize->dequantize pair applied to a float scalar should preserve the
403 /// value (up to the precision lost by dividing/multiplying with the scale).
quantizeScalarTest(float val,ElemKind qTy,quantization::Schema schema)404 void quantizeScalarTest(float val, ElemKind qTy, quantization::Schema schema) {
405 ExecutionEngine EE{};
406 auto &mod = EE.getModule();
407 Function *F = mod.createFunction("main");
408 PlaceholderBindings bindings;
409
410 // Choose quantization parameters
411 auto TQP = quantization::chooseQuantizationParams({val, val}, schema, qTy);
412
413 // Create quantize/dequantize network for a single float value
414 auto *input = mod.createPlaceholder(ElemKind::FloatTy, {1}, "val", false);
415 auto inputQTy = mod.uniqueType(qTy, {1}, TQP.scale, TQP.offset);
416 QuantizeNode *quant = F->createQuantize("quant", input, inputQTy);
417 DequantizeNode *dequant =
418 F->createDequantize("dequant", quant, ElemKind::FloatTy);
419 SaveNode *save = F->createSave("save", dequant);
420
421 // Allocate placeholders, set input, run, get output
422 auto inpH = bindings.allocate(input)->getHandle();
423 auto outH = bindings.allocate(save->getPlaceholder())->getHandle();
424 inpH.at({0}) = val;
425 EE.compile(CompilationMode::Infer);
426 EE.run(bindings);
427 float outVal = outH.raw(0);
428 EXPECT_NEAR(val, outVal, 0.0000000001);
429 }
430
TEST(Quantization,quantizeScalarTestInt8)431 TEST(Quantization, quantizeScalarTestInt8) {
432 quantizeScalarTest(0.0, ElemKind::Int8QTy, quantization::Schema::Asymmetric);
433 quantizeScalarTest(0.0, ElemKind::Int8QTy, quantization::Schema::Symmetric);
434 quantizeScalarTest(0.0, ElemKind::Int8QTy,
435 quantization::Schema::SymmetricWithUnsigned);
436 quantizeScalarTest(1.3, ElemKind::Int8QTy, quantization::Schema::Asymmetric);
437 quantizeScalarTest(1.3, ElemKind::Int8QTy, quantization::Schema::Symmetric);
438 quantizeScalarTest(1.3, ElemKind::Int8QTy,
439 quantization::Schema::SymmetricWithUnsigned);
440 quantizeScalarTest(-1.3, ElemKind::Int8QTy, quantization::Schema::Asymmetric);
441 quantizeScalarTest(-1.3, ElemKind::Int8QTy, quantization::Schema::Symmetric);
442 quantizeScalarTest(-1.3, ElemKind::Int8QTy,
443 quantization::Schema::SymmetricWithUnsigned);
444 }
445
TEST(Quantization,quantizeScalarTestInt16)446 TEST(Quantization, quantizeScalarTestInt16) {
447 quantizeScalarTest(0.0, ElemKind::Int16QTy, quantization::Schema::Asymmetric);
448 quantizeScalarTest(0.0, ElemKind::Int16QTy, quantization::Schema::Symmetric);
449 quantizeScalarTest(0.0, ElemKind::Int16QTy,
450 quantization::Schema::SymmetricWithUnsigned);
451 quantizeScalarTest(1.3, ElemKind::Int16QTy, quantization::Schema::Asymmetric);
452 quantizeScalarTest(1.3, ElemKind::Int16QTy, quantization::Schema::Symmetric);
453 quantizeScalarTest(1.3, ElemKind::Int16QTy,
454 quantization::Schema::SymmetricWithUnsigned);
455 quantizeScalarTest(-1.3, ElemKind::Int16QTy,
456 quantization::Schema::Asymmetric);
457 quantizeScalarTest(-1.3, ElemKind::Int16QTy, quantization::Schema::Symmetric);
458 quantizeScalarTest(-1.3, ElemKind::Int16QTy,
459 quantization::Schema::SymmetricWithUnsigned);
460 }
461
TEST(Quantization,quantizeScalarTestInt32)462 TEST(Quantization, quantizeScalarTestInt32) {
463 quantizeScalarTest(0.0, ElemKind::Int32QTy, quantization::Schema::Asymmetric);
464 quantizeScalarTest(0.0, ElemKind::Int32QTy, quantization::Schema::Symmetric);
465 quantizeScalarTest(0.0, ElemKind::Int32QTy,
466 quantization::Schema::SymmetricWithUnsigned);
467 quantizeScalarTest(1.3, ElemKind::Int32QTy, quantization::Schema::Asymmetric);
468 quantizeScalarTest(1.3, ElemKind::Int32QTy, quantization::Schema::Symmetric);
469 quantizeScalarTest(1.3, ElemKind::Int32QTy,
470 quantization::Schema::SymmetricWithUnsigned);
471 quantizeScalarTest(-1.3, ElemKind::Int32QTy,
472 quantization::Schema::Asymmetric);
473 quantizeScalarTest(-1.3, ElemKind::Int32QTy, quantization::Schema::Symmetric);
474 quantizeScalarTest(-1.3, ElemKind::Int32QTy,
475 quantization::Schema::SymmetricWithUnsigned);
476 }
477
478 /// Check corner case when bias is quantized as int32 with unconstrained
479 /// scale and offset parameters and used within a subtraction bias - biasOffset
480 /// which is expected to be within int32 limits.
quantizeBiasInt32CornerCaseTest(float val)481 static void quantizeBiasInt32CornerCaseTest(float val) {
482 // Choose bias quantization parameters
483 float biasF = val;
484 auto biasTQP = quantization::chooseQuantizationParams(
485 {biasF, biasF}, quantization::Schema::Asymmetric, ElemKind::Int32QTy);
486
487 // Quantize the tensor.
488 Tensor biasTF(ElemKind::FloatTy, {1});
489 biasTF.getHandle<float>().at({0}) = biasF;
490 auto biasTQ =
491 quantization::quantizeTensor(biasTF, biasTQP, ElemKind::Int32QTy);
492 int32_t biasQ = biasTQ.getHandle<int32_t>().at({0});
493 int32_t biasOffset = biasTQP.offset;
494
495 // Compute difference and check against int32 limits.
496 int64_t diff = ((int64_t)biasQ) - ((int64_t)biasOffset);
497 EXPECT_TRUE(std::numeric_limits<int32_t>::min() <= diff);
498 EXPECT_TRUE(diff <= std::numeric_limits<int32_t>::max());
499 }
500
TEST(Quantization,quantizeBiasInt32CornerCaseTests)501 TEST(Quantization, quantizeBiasInt32CornerCaseTests) {
502 quantizeBiasInt32CornerCaseTest(0.0);
503 quantizeBiasInt32CornerCaseTest(0.3);
504 quantizeBiasInt32CornerCaseTest(-0.3);
505 quantizeBiasInt32CornerCaseTest(0.0000003);
506 quantizeBiasInt32CornerCaseTest(-0.0000003);
507 quantizeBiasInt32CornerCaseTest(30000000.0);
508 quantizeBiasInt32CornerCaseTest(-30000000.0);
509 }
510
511 /// Verify the quantization utility function which performs finer grained
512 /// quantization along a given dimension for given \p qSchema and \p qTy.
513 template <class eTy>
quantizeTensorRowwise(quantization::Schema qSchema,ElemKind qTy)514 static void quantizeTensorRowwise(quantization::Schema qSchema, ElemKind qTy) {
515 dim_t numCols = 20;
516 dim_t qDim = 0;
517 dim_t qStep = 1;
518
519 // Initialize tensors.
520 Tensor tensor(ElemKind::FloatTy, {2, numCols});
521 Tensor row1(ElemKind::FloatTy, {numCols});
522 Tensor row2(ElemKind::FloatTy, {numCols});
523 auto tensorH = tensor.getHandle<float>();
524 auto row1H = row1.getHandle<float>();
525 auto row2H = row2.getHandle<float>();
526 for (dim_t idx = 0; idx < numCols; idx++) {
527 tensorH.at({0, idx}) = float(idx);
528 tensorH.at({1, idx}) = float(idx) - 128.0;
529 row1H.raw(idx) = float(idx);
530 row2H.raw(idx) = float(idx) - 128.0;
531 }
532
533 // Quantize rowwise using specialized function.
534 Tensor scales(ElemKind::FloatTy, {2});
535 Tensor offsets(ElemKind::Int32ITy, {2});
536 getTensorQuantizationParams(tensor, scales, offsets, qSchema, qTy, qDim,
537 qStep);
538 Tensor tensorQ =
539 quantization::quantizeTensor(tensor, scales, offsets, qTy, qDim, qStep);
540 auto tensorQH = tensorQ.getHandle<eTy>();
541 auto scalesH = scales.getHandle<float>();
542 auto offsetsH = offsets.getHandle<int32_t>();
543
544 // Quantize rowwise using per-tensor functions.
545 float row1Min = tensorH.at({0, 0});
546 float row1Max = tensorH.at({0, numCols - 1});
547 float row2Min = tensorH.at({1, 0});
548 float row2Max = tensorH.at({1, numCols - 1});
549 auto TQP1 =
550 quantization::chooseQuantizationParams({row1Min, row1Max}, qSchema, qTy);
551 auto TQP2 =
552 quantization::chooseQuantizationParams({row2Min, row2Max}, qSchema, qTy);
553 Tensor row1Q = quantization::quantizeTensor(row1, TQP1, qTy);
554 Tensor row2Q = quantization::quantizeTensor(row2, TQP2, qTy);
555 auto row1QH = row1Q.getHandle<eTy>();
556 auto row2QH = row2Q.getHandle<eTy>();
557
558 // Check.
559 EXPECT_EQ(TQP1.scale, scalesH.raw(0));
560 EXPECT_EQ(TQP2.scale, scalesH.raw(1));
561 EXPECT_EQ(TQP1.offset, offsetsH.raw(0));
562 EXPECT_EQ(TQP2.offset, offsetsH.raw(1));
563 for (dim_t idx = 0; idx < 3; idx++) {
564 EXPECT_EQ(tensorQH.at({0, idx}), row1QH.raw(idx));
565 EXPECT_EQ(tensorQH.at({1, idx}), row2QH.raw(idx));
566 }
567 }
568
TEST(Quantization,QuantizeTensorRowwiseTest)569 TEST(Quantization, QuantizeTensorRowwiseTest) {
570 quantizeTensorRowwise<int8_t>(quantization::Schema::Asymmetric,
571 ElemKind::Int8QTy);
572 quantizeTensorRowwise<int16_t>(quantization::Schema::Asymmetric,
573 ElemKind::Int16QTy);
574 quantizeTensorRowwise<int32_t>(quantization::Schema::Asymmetric,
575 ElemKind::Int32QTy);
576 quantizeTensorRowwise<int8_t>(quantization::Schema::Symmetric,
577 ElemKind::Int8QTy);
578 quantizeTensorRowwise<int16_t>(quantization::Schema::Symmetric,
579 ElemKind::Int16QTy);
580 quantizeTensorRowwise<int32_t>(quantization::Schema::Symmetric,
581 ElemKind::Int32QTy);
582 quantizeTensorRowwise<int8_t>(quantization::Schema::SymmetricWithUnsigned,
583 ElemKind::Int8QTy);
584 quantizeTensorRowwise<int16_t>(quantization::Schema::SymmetricWithUnsigned,
585 ElemKind::Int16QTy);
586 quantizeTensorRowwise<int32_t>(quantization::Schema::SymmetricWithUnsigned,
587 ElemKind::Int32QTy);
588 quantizeTensorRowwise<int8_t>(quantization::Schema::SymmetricWithPower2Scale,
589 ElemKind::Int8QTy);
590 quantizeTensorRowwise<int16_t>(quantization::Schema::SymmetricWithPower2Scale,
591 ElemKind::Int16QTy);
592 quantizeTensorRowwise<int32_t>(quantization::Schema::SymmetricWithPower2Scale,
593 ElemKind::Int32QTy);
594 }
595
596 /// Helper for quantizing a simple Conv with precision \p quantizationPrecision
597 /// while the bias is quantized using \p quantizationPrecisionBias.
quantizeSimpleConvGraph(ElemKind quantizationPrecision,ElemKind quantizationPrecisionBias)598 static void quantizeSimpleConvGraph(ElemKind quantizationPrecision,
599 ElemKind quantizationPrecisionBias) {
600 ExecutionEngine EE{};
601 auto &mod = EE.getModule();
602 Function *F = mod.createFunction("main");
603
604 auto *input =
605 mod.createPlaceholder(ElemKind::FloatTy, {1, 4, 4, 1}, "input", false);
606 auto *filter = mod.createConstant(ElemKind::FloatTy, {2, 2, 2, 1}, "filter");
607 auto *bias = mod.createConstant(ElemKind::FloatTy, {2}, "bias");
608 auto outTy = mod.uniqueType(ElemKind::FloatTy, {1, 4, 8, 2});
609 PlaceholderBindings bindings;
610 bindings.allocate(input);
611 filter->getHandle().randomize(-1.0, 1.0, mod.getPRNG());
612 bias->getHandle().randomize(-1.0, 1.0, mod.getPRNG());
613
614 auto *CN = F->createConv("Conv", input, filter, bias, outTy, {2, 2}, {1, 1},
615 {0, 2, 1, 3}, 1);
616 auto *S = F->createSave("ret", CN);
617 bindings.allocate(S->getPlaceholder());
618
619 quantization::QuantizationConfiguration quantConfig{{
620 {input->getOutput().generateNodeOutputName(), {0.0f, 2.0f}},
621 {filter->getOutput().generateNodeOutputName(), {0.0f, 3.0f}},
622 {bias->getOutput().generateNodeOutputName(), {0.0f, 4.0f}},
623 {CN->getResult().generateNodeOutputName(), {0.0f, 6.0f}},
624 }};
625
626 quantConfig.precision = quantizationPrecision;
627 quantConfig.precisionBias = quantizationPrecisionBias;
628 quantConfig.assertAllNodesQuantized = true;
629 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
630 quantization::quantizeFunction(F, quantConfig, *backend);
631
632 // Make sure that graph can be compiled and run.
633 EE.compile(CompilationMode::Infer);
634 EE.run(bindings);
635 }
636
637 /// Test that a simple Conv graph can be quantized in Int8QTy and Int8QTy bias.
TEST(Quantization,QuantizeGraph_Int8_BiasInt8)638 TEST(Quantization, QuantizeGraph_Int8_BiasInt8) {
639 quantizeSimpleConvGraph(ElemKind::Int8QTy, ElemKind::Int8QTy);
640 }
641
642 /// Test that a simple Conv graph can be quantized in Int8QTy and Int32QTy bias.
TEST(Quantization,QuantizeGraph_Int8_BiasInt32)643 TEST(Quantization, QuantizeGraph_Int8_BiasInt32) {
644 quantizeSimpleConvGraph(ElemKind::Int8QTy, ElemKind::Int32QTy);
645 }
646
647 /// Test that a simple Conv graph can be quantized in Int16QTy and Int16QTy
648 /// bias.
TEST(Quantization,QuantizeGraph_Int16_BiasInt16)649 TEST(Quantization, QuantizeGraph_Int16_BiasInt16) {
650 quantizeSimpleConvGraph(ElemKind::Int16QTy, ElemKind::Int16QTy);
651 }
652
653 /// Test that a simple Conv graph can be quantized in Int16QTy and Int32QTy
654 /// bias.
TEST(Quantization,QuantizeGraph_Int16_BiasInt32)655 TEST(Quantization, QuantizeGraph_Int16_BiasInt32) {
656 quantizeSimpleConvGraph(ElemKind::Int16QTy, ElemKind::Int32QTy);
657 }
658
659 /// Test that when a node is quantized before its users are quantized then the
660 /// users correctly find the quantization parameters. This tests that updating
661 /// the nodeToTQP_ map in FunctionQuantizer::postProcessing() works correctly.
TEST(Quantization,TestQuantizedInputBeforeQuantizedNode)662 TEST(Quantization, TestQuantizedInputBeforeQuantizedNode) {
663 ExecutionEngine EE{};
664 auto &mod = EE.getModule();
665 Function *F = mod.createFunction("main");
666
667 auto *input = mod.createPlaceholder(ElemKind::FloatTy, {3}, "input", true);
668 PlaceholderBindings bindings;
669 bindings.allocate(input);
670
671 // Note: Intentionally add successive reshapes so the GraphOptimizer merges
672 // them and creates a new one. This way the newly created Reshape will be
673 // placed at the end of the list of nodes in F, and then it will be quantized
674 // before SN. I think this is the most straightforward way to cover the logic
675 // path inside FunctionQuantizer::postProcessing() that updates nodeToTQP_.
676 auto *reshape1 = F->createReshape("reshape1", input, {3, 1});
677 auto *reshape2 = F->createReshape("reshape2", reshape1, {1, 3});
678 auto *SN = F->createSlice("slice", reshape2, {0, 1}, {1, 2});
679 auto *S = F->createSave("ret", SN);
680 bindings.allocate(S->getPlaceholder());
681
682 // We need to optimize here first so that the two reshapes are merged.
683 optimize(F, CompilationMode::Infer);
684
685 ReshapeNode *newReshape = llvm::dyn_cast<ReshapeNode>(SN->getInput());
686 ASSERT_TRUE(newReshape);
687
688 quantization::QuantizationConfiguration quantConfig{{
689 {input->getOutput().generateNodeOutputName(), {-1.0, 1.0}},
690 {newReshape->getResult().generateNodeOutputName(), {-1.0, 1.0}},
691 {NodeValue::generateNodeOutputName(SN->getName()), {-1.0, 1.0}},
692 }};
693
694 quantConfig.assertAllNodesQuantized = true;
695 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
696 quantization::quantizeFunction(F, quantConfig, *backend);
697
698 // Remove unnecessary conversions.
699 optimize(F, CompilationMode::Infer);
700
701 // Now we verify that the SliceNode was in fact quantized.
702 {
703 auto *saveNode = llvm::dyn_cast<SaveNode>(F->getNodeByName(S->getName()));
704 ASSERT_TRUE(saveNode);
705 auto *deqNode =
706 llvm::dyn_cast<DequantizeNode>(saveNode->getInput().getNode());
707 ASSERT_TRUE(deqNode);
708 auto *sliceNode = llvm::dyn_cast<SliceNode>(deqNode->getInput().getNode());
709 ASSERT_TRUE(sliceNode);
710 EXPECT_TRUE(sliceNode->getResult().getType()->isQuantizedType());
711 }
712 }
713
714 /// Test enabling RowwiseQuantizedFullyConnected in Glow quantization
715 /// procedure. A FC can be quantized and converted to a
716 /// RowwiseQuantizedFullyConnected if:
717 /// 1. The weights of FC is constant;
718 /// 2. Use -enable-rowwise option or set enableRowwise param in
719 /// quantization::quantizeFunction to true. In unittest, the later one is used.
720 static void
enableRowwiseQuantizedFullyConnected(ElemKind quantizationPrecision,ElemKind quantizationPrecisionBias)721 enableRowwiseQuantizedFullyConnected(ElemKind quantizationPrecision,
722 ElemKind quantizationPrecisionBias) {
723 ExecutionEngine EE{};
724 auto &mod = EE.getModule();
725 Function *F = mod.createFunction("main");
726
727 auto *input = mod.createPlaceholder(ElemKind::FloatTy, {1, 3}, "input", true);
728 auto *W = mod.createPlaceholder(ElemKind::FloatTy, {3, 2}, "weights", true);
729 auto *B = mod.createPlaceholder(ElemKind::FloatTy, {2}, "bias", true);
730 PlaceholderBindings bindings;
731 bindings.allocate(input);
732 bindings.allocate(W)->init(Tensor::InitKind::Xavier, 3, mod.getPRNG());
733 bindings.allocate(B)->init(Tensor::InitKind::Broadcast, 0.1, mod.getPRNG());
734
735 auto *WC = mod.createConstant(ElemKind::FloatTy, W->dims(), "wc");
736 auto *FC = F->createFullyConnected("FC", input, WC, B);
737 auto *S = F->createSave("ret", FC);
738 bindings.allocate(S->getPlaceholder());
739
740 LoweredInfoMap loweredMapForQuant;
741 CompilationContext cctx(/* bindings */ nullptr, &loweredMapForQuant);
742 ::glow::lower(F, cctx);
743
744 // Get the MatMul node and the Batched_Add node.
745 MatMulNode *matMul;
746 BatchedAddNode *batchedAdd;
747 for (Node &N : F->getNodes()) {
748 if (N.getKind() == Kinded::Kind::MatMulNodeKind) {
749 matMul = llvm::cast<MatMulNode>(&N);
750 }
751 if (N.getKind() == Kinded::Kind::BatchedAddNodeKind) {
752 batchedAdd = llvm::cast<BatchedAddNode>(&N);
753 }
754 }
755 ASSERT_TRUE(matMul);
756 ASSERT_TRUE(batchedAdd);
757
758 quantization::QuantizationConfiguration quantConfig{{
759 {input->getOutput().generateNodeOutputName(), {0.2f, 2.0f}},
760 {WC->getOutput().generateNodeOutputName(), {0.3f, 3.0f}},
761 {B->getOutput().generateNodeOutputName(), {0.4f, 4.0f}},
762 {matMul->getResult().generateNodeOutputName(), {0.6f, 6.0f}},
763 {batchedAdd->getResult().generateNodeOutputName(), {0.6f, 6.0f}},
764 }};
765
766 quantConfig.precision = quantizationPrecision;
767 quantConfig.precisionBias = quantizationPrecisionBias;
768 quantConfig.enableRowwise = true;
769 quantConfig.assertAllNodesQuantized = true;
770 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
771 quantization::quantizeFunction(F, quantConfig, *backend, loweredMapForQuant);
772
773 // Check the graph structure after quantization.
774 auto *saveNode = llvm::dyn_cast<SaveNode>(F->getNodeByName(S->getName()));
775 ASSERT_TRUE(saveNode);
776 auto *deqNode =
777 llvm::dyn_cast<DequantizeNode>(saveNode->getInput().getNode());
778 ASSERT_TRUE(deqNode);
779 auto *rwNode = llvm::dyn_cast<RowwiseQuantizedFullyConnectedNode>(
780 deqNode->getInput().getNode());
781 ASSERT_TRUE(rwNode);
782 auto *inNode = llvm::dyn_cast<QuantizeNode>(rwNode->getInput().getNode());
783 ASSERT_TRUE(inNode);
784 auto *biasNode = llvm::dyn_cast<QuantizeNode>(rwNode->getBias().getNode());
785 ASSERT_TRUE(biasNode);
786 auto *weightsNode = llvm::dyn_cast<Constant>(rwNode->getWeights().getNode());
787 ASSERT_TRUE(weightsNode);
788 auto *scalesNode = llvm::dyn_cast<Constant>(rwNode->getScales().getNode());
789 ASSERT_TRUE(scalesNode);
790 auto *offsetsNode = llvm::dyn_cast<Constant>(rwNode->getOffsets().getNode());
791 ASSERT_TRUE(offsetsNode);
792
793 // Make sure that graph can be compiled and run. We check the correctness of
794 // RowwiseQuantizedFullyConnected in operatorTests.cpp.
795 EE.compile(CompilationMode::Infer);
796
797 EE.run(bindings);
798 }
799
TEST(Quantization,enableRowwiseQuantizedFullyConnected_Int8_BiasInt8)800 TEST(Quantization, enableRowwiseQuantizedFullyConnected_Int8_BiasInt8) {
801 enableRowwiseQuantizedFullyConnected(ElemKind::Int8QTy, ElemKind::Int8QTy);
802 }
803
TEST(Quantization,enableRowwiseQuantizedFullyConnected_Int8_BiasInt32)804 TEST(Quantization, enableRowwiseQuantizedFullyConnected_Int8_BiasInt32) {
805 enableRowwiseQuantizedFullyConnected(ElemKind::Int8QTy, ElemKind::Int32QTy);
806 }
807
808 /// Test enabling RowwiseQuantizedFullyConnected with Symmetric quantization.
TEST(Quantization,enableRowwiseQuantizedFullyConnectedSymmetric)809 TEST(Quantization, enableRowwiseQuantizedFullyConnectedSymmetric) {
810 ExecutionEngine EE{};
811 auto &mod = EE.getModule();
812 PlaceholderBindings bindings;
813 Function *F = mod.createFunction("main");
814
815 auto *input = mod.createPlaceholder(ElemKind::FloatTy, {10, 80}, "in", false);
816 auto *FC = F->createFullyConnected(bindings, "FC", input, 100);
817 auto *res = F->createSave("save", FC);
818 bindings.allocate(res->getPlaceholder());
819 bindings.allocate(input);
820 bindings.get(input)->getHandle().randomize(-1.0, 6.0, mod.getPRNG());
821
822 ::glow::convertPlaceholdersToConstants(F, bindings,
823 {input, res->getPlaceholder()});
824
825 // Note that we generate values for the Weights because they will be used
826 // during rowwise-quantization to select each row's scale/offset.
827 auto *WC = llvm::cast<Constant>(FC->getWeights());
828 WC->getPayloadMutable().getHandle().randomize(-0.7, 1.1, mod.getPRNG());
829 auto *BC = llvm::cast<Constant>(FC->getBias());
830
831 TensorProfilingParams inputTPP = {-1.0, 6.0};
832 TensorProfilingParams matmulTPP = {0.0, 10.0};
833 TensorProfilingParams batchedaddTPP = {0.0, 10.0};
834 TensorProfilingParams biasTPP = {0, 20};
835
836 TensorQuantizationParams inputTQP = chooseQuantizationParams(
837 inputTPP, quantization::Schema::Symmetric, ElemKind::Int8QTy);
838 TensorQuantizationParams matmulTQP = chooseQuantizationParams(
839 matmulTPP, quantization::Schema::Symmetric, ElemKind::Int8QTy);
840 TensorQuantizationParams batchedaddTQP = chooseQuantizationParams(
841 batchedaddTPP, quantization::Schema::Symmetric, ElemKind::Int8QTy);
842 TensorQuantizationParams biasTQP = chooseQuantizationParams(
843 biasTPP, quantization::Schema::Symmetric, ElemKind::Int8QTy);
844
845 EXPECT_EQ(inputTQP.offset, 0);
846 EXPECT_EQ(matmulTQP.offset, 0);
847 EXPECT_EQ(batchedaddTQP.offset, 0);
848 EXPECT_EQ(biasTQP.offset, 0);
849
850 LoweredInfoMap loweredMapForQuant;
851 CompilationContext cctx(/* bindings */ nullptr, &loweredMapForQuant);
852 ::glow::lower(F, cctx);
853
854 // Get the MatMul node and the Batched_Add node.
855 MatMulNode *matMul;
856 BatchedAddNode *batchedAdd;
857 for (Node &N : F->getNodes()) {
858 if (N.getKind() == Kinded::Kind::MatMulNodeKind) {
859 matMul = llvm::cast<MatMulNode>(&N);
860 }
861 if (N.getKind() == Kinded::Kind::BatchedAddNodeKind) {
862 batchedAdd = llvm::cast<BatchedAddNode>(&N);
863 }
864 }
865 ASSERT_TRUE(matMul);
866 ASSERT_TRUE(batchedAdd);
867
868 // Note: Using dummy offset for the weights, as it should be
869 // rowwise-quantized.
870 quantization::QuantizationConfiguration quantConfig{{
871 {input->getOutput().generateNodeOutputName(), inputTPP},
872 {WC->getOutput().generateNodeOutputName(), {-0.7, 1.1}},
873 {BC->getOutput().generateNodeOutputName(), biasTPP},
874 {matMul->getResult().generateNodeOutputName(), matmulTPP},
875 {batchedAdd->getResult().generateNodeOutputName(), batchedaddTPP},
876 }};
877
878 quantConfig.schema = quantization::Schema::Symmetric;
879 quantConfig.enableRowwise = true;
880 quantConfig.assertAllNodesQuantized = true;
881 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
882 quantization::quantizeFunction(F, quantConfig, *backend, loweredMapForQuant);
883
884 // Check the graph structure after quantization.
885 auto *saveNode = llvm::dyn_cast<SaveNode>(F->getNodeByName(res->getName()));
886 ASSERT_TRUE(saveNode);
887 auto *deqNode =
888 llvm::dyn_cast<DequantizeNode>(saveNode->getInput().getNode());
889 ASSERT_TRUE(deqNode);
890 auto *rwNode = llvm::dyn_cast<RowwiseQuantizedFullyConnectedNode>(
891 deqNode->getInput().getNode());
892 ASSERT_TRUE(rwNode);
893 auto *inNode = llvm::dyn_cast<QuantizeNode>(rwNode->getInput().getNode());
894 ASSERT_TRUE(inNode);
895 auto *biasNode = llvm::dyn_cast<QuantizeNode>(rwNode->getBias().getNode());
896 ASSERT_TRUE(biasNode);
897 auto *weightsNode = llvm::dyn_cast<Constant>(rwNode->getWeights().getNode());
898 ASSERT_TRUE(weightsNode);
899 auto *scalesNode = llvm::dyn_cast<Constant>(rwNode->getScales().getNode());
900 ASSERT_TRUE(scalesNode);
901 auto *offsetsNode = llvm::dyn_cast<Constant>(rwNode->getOffsets().getNode());
902 ASSERT_TRUE(offsetsNode);
903
904 // Because we're using symmetric quantization, the offsets should all be zero.
905 const auto offsetsH = offsetsNode->getPayload().getHandle<int32_t>();
906 EXPECT_TRUE(offsetsH.isZero());
907
908 // Make sure that graph can be compiled and run. We check the correctness of
909 // RowwiseQuantizedFullyConnected in operatorTests.cpp.
910 EE.compile(CompilationMode::Infer);
911
912 EE.run(bindings);
913 }
914
915 /// Test enabling ChannelwiseQuantizedConv2D in the quantization procedure.
916 /// A standard Convolution node can be quantized and converted to a
917 /// ChannelwiseQuantizedConvolution if:
918 /// 1. The filter and bias are constants.
919 /// 2. Use -enable-channelwise option or set enableChannelwise param in
920 /// quantization::quantizeFunction to true.
enableChannelwiseQuantizedConv2D(ElemKind qPrec,ElemKind qPrecBias,quantization::Schema schema)921 static void enableChannelwiseQuantizedConv2D(ElemKind qPrec, ElemKind qPrecBias,
922 quantization::Schema schema) {
923 ExecutionEngine EE{};
924 auto &mod = EE.getModule();
925 Function *F = mod.createFunction("main");
926 PlaceholderBindings bindings;
927
928 // Convolution parameters.
929 std::vector<dim_t> inputDims = {5, 3, 3, 2};
930 std::vector<dim_t> filterDims = {4, 2, 2, 1};
931 std::vector<dim_t> biasDims = {4};
932 std::vector<dim_t> outputDims = {5, 2, 2, 4};
933 std::vector<unsigned_t> kernels = {2, 2};
934 std::vector<unsigned_t> strides = {1, 1};
935 std::vector<unsigned_t> pads = {0, 0, 0, 0};
936 dim_t group = 2;
937 dim_t dilation = 1;
938
939 // Create input placeholder.
940 auto *input =
941 mod.createPlaceholder(ElemKind::FloatTy, inputDims, "input", false);
942 bindings.allocate(input)->getHandle<float>().randomize(-1.0, 1.0,
943 mod.getPRNG());
944
945 // Create filter constant.
946 auto *filterC = mod.createConstant(ElemKind::FloatTy, filterDims, "filterC");
947 filterC->getPayloadMutable().getHandle<float>().randomize(-1.0, 1.0,
948 mod.getPRNG());
949
950 // Create bias constant.
951 auto *biasC = mod.createConstant(ElemKind::FloatTy, biasDims, "biasC");
952 biasC->getPayloadMutable().getHandle<float>().randomize(-1.0, 1.0,
953 mod.getPRNG());
954
955 // Create Convolution.
956 auto *outTy = mod.uniqueType(ElemKind::FloatTy, outputDims);
957 ConvolutionNode *conv =
958 F->createConv("Conv", input, filterC, biasC, outTy, kernels, strides,
959 pads, group, dilation);
960 SaveNode *save = F->createSave("save", conv);
961 bindings.allocate(save->getPlaceholder());
962
963 // Quantize function. Choose asymmetric ranges to test quantization params.
964 quantization::QuantizationConfiguration quantConfig{{
965 {input->getOutput().generateNodeOutputName(), {-2.0, 1.0}},
966 {filterC->getOutput().generateNodeOutputName(), {-1.0, 2.0}},
967 {biasC->getOutput().generateNodeOutputName(), {0.0, 3.0}},
968 {conv->getResult().generateNodeOutputName(), {-3.0, 0.0}},
969 }};
970 quantConfig.schema = schema;
971 quantConfig.precision = qPrec;
972 quantConfig.precisionBias = qPrecBias;
973 quantConfig.enableChannelwise = true;
974 quantConfig.assertAllNodesQuantized = true;
975 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
976 quantization::quantizeFunction(F, quantConfig, *backend);
977
978 // Check the graph structure after quantization.
979 auto *saveNode = llvm::dyn_cast<SaveNode>(F->getNodeByName(save->getName()));
980 ASSERT_TRUE(saveNode);
981 auto *deqNode =
982 llvm::dyn_cast<DequantizeNode>(saveNode->getInput().getNode());
983 ASSERT_TRUE(deqNode);
984 auto *cwqConvNode = llvm::dyn_cast<ChannelwiseQuantizedConvolutionNode>(
985 deqNode->getInput().getNode());
986 ASSERT_TRUE(cwqConvNode);
987 auto *inputNode =
988 llvm::dyn_cast<QuantizeNode>(cwqConvNode->getInput().getNode());
989 ASSERT_TRUE(inputNode);
990 auto *filterNode =
991 llvm::dyn_cast<Constant>(cwqConvNode->getFilter().getNode());
992 ASSERT_TRUE(filterNode);
993 auto *biasNode = llvm::dyn_cast<Constant>(cwqConvNode->getBias().getNode());
994 ASSERT_TRUE(biasNode);
995 auto *filterScalesNode =
996 llvm::dyn_cast<Constant>(cwqConvNode->getFilterScales().getNode());
997 ASSERT_TRUE(filterScalesNode);
998 auto *filterOffsetsNode =
999 llvm::dyn_cast<Constant>(cwqConvNode->getFilterOffsets().getNode());
1000 ASSERT_TRUE(filterOffsetsNode);
1001 auto *biasScalesNode =
1002 llvm::dyn_cast<Constant>(cwqConvNode->getBiasScales().getNode());
1003 ASSERT_TRUE(biasScalesNode);
1004 auto *biasOffsetsNode =
1005 llvm::dyn_cast<Constant>(cwqConvNode->getBiasOffsets().getNode());
1006 ASSERT_TRUE(biasOffsetsNode);
1007
1008 // Check precisions.
1009 ASSERT_EQ(inputNode->getResult().getElementType(), qPrec);
1010 ASSERT_EQ(filterNode->getOutput().getElementType(), qPrec);
1011 ASSERT_EQ(biasNode->getOutput().getElementType(), qPrecBias);
1012 ASSERT_EQ(filterScalesNode->getOutput().getElementType(), ElemKind::FloatTy);
1013 ASSERT_EQ(filterOffsetsNode->getOutput().getElementType(),
1014 ElemKind::Int32ITy);
1015 ASSERT_EQ(biasScalesNode->getOutput().getElementType(), ElemKind::FloatTy);
1016 ASSERT_EQ(biasOffsetsNode->getOutput().getElementType(), ElemKind::Int32ITy);
1017 ASSERT_EQ(cwqConvNode->getResult().getElementType(), qPrec);
1018
1019 // Check quantization parameters.
1020 validateQuantizationParams({inputNode->getResult().getType()->getScale(),
1021 inputNode->getResult().getType()->getOffset()},
1022 schema, qPrec);
1023 validateQuantizationParams({cwqConvNode->getResult().getType()->getScale(),
1024 cwqConvNode->getResult().getType()->getOffset()},
1025 schema, qPrec);
1026 for (dim_t idx = 0; idx < outputDims[3]; idx++) {
1027 auto filterScalesH = filterScalesNode->getPayload().getHandle<float>();
1028 auto filterOffsetsH = filterOffsetsNode->getPayload().getHandle<int32_t>();
1029 auto biasScalesH = biasScalesNode->getPayload().getHandle<float>();
1030 auto biasOffsetsH = biasOffsetsNode->getPayload().getHandle<int32_t>();
1031 validateQuantizationParams(
1032 {filterScalesH.raw(idx), filterOffsetsH.raw(idx)}, schema, qPrec);
1033 validateQuantizationParams({biasScalesH.raw(idx), biasOffsetsH.raw(idx)},
1034 schema, qPrecBias);
1035 }
1036
1037 // Make sure that graph can be compiled and run. We check the correctness of
1038 // ChannelwiseQuantizedConvolution in OperatorTest.cpp.
1039 EE.compile(CompilationMode::Infer);
1040 EE.run(bindings);
1041 }
1042
TEST(Quantization,enableChannelwiseQuantizedConv2D_Int8_BiasInt8)1043 TEST(Quantization, enableChannelwiseQuantizedConv2D_Int8_BiasInt8) {
1044 enableChannelwiseQuantizedConv2D(ElemKind::Int8QTy, ElemKind::Int8QTy,
1045 quantization::Schema::Asymmetric);
1046 enableChannelwiseQuantizedConv2D(ElemKind::Int8QTy, ElemKind::Int8QTy,
1047 quantization::Schema::Symmetric);
1048 enableChannelwiseQuantizedConv2D(ElemKind::Int8QTy, ElemKind::Int8QTy,
1049 quantization::Schema::SymmetricWithUnsigned);
1050 enableChannelwiseQuantizedConv2D(
1051 ElemKind::Int8QTy, ElemKind::Int8QTy,
1052 quantization::Schema::SymmetricWithPower2Scale);
1053 }
1054
TEST(Quantization,enableChannelwiseQuantizedConv2D_Int8_BiasInt32)1055 TEST(Quantization, enableChannelwiseQuantizedConv2D_Int8_BiasInt32) {
1056 enableChannelwiseQuantizedConv2D(ElemKind::Int8QTy, ElemKind::Int32QTy,
1057 quantization::Schema::Asymmetric);
1058 enableChannelwiseQuantizedConv2D(ElemKind::Int8QTy, ElemKind::Int32QTy,
1059 quantization::Schema::Symmetric);
1060 enableChannelwiseQuantizedConv2D(ElemKind::Int8QTy, ElemKind::Int32QTy,
1061 quantization::Schema::SymmetricWithUnsigned);
1062 enableChannelwiseQuantizedConv2D(
1063 ElemKind::Int8QTy, ElemKind::Int32QTy,
1064 quantization::Schema::SymmetricWithPower2Scale);
1065 }
1066
1067 /// Check that SLWS is correctly fused rowwise-quantized by the quantizer.
TEST(Quantization,enableRowwiseQuantizedSLWS)1068 TEST(Quantization, enableRowwiseQuantizedSLWS) {
1069 ExecutionEngine EE{};
1070 auto &mod = EE.getModule();
1071 Function *F = mod.createFunction("main");
1072 PlaceholderBindings bindings;
1073
1074 auto *data = mod.createPlaceholder(ElemKind::FloatTy, {3, 1}, "data", false);
1075 auto *weights =
1076 mod.createPlaceholder(ElemKind::FloatTy, {8}, "weights", false);
1077 auto *indices =
1078 mod.createPlaceholder(ElemKind::Int64ITy, {8}, "indices", false);
1079 auto *lengths =
1080 mod.createPlaceholder(ElemKind::Int32ITy, {4}, "lengths", false);
1081
1082 // Don't worry about allocating them as we are not going to run anyway.
1083 bindings.allocate(data);
1084 bindings.allocate(weights);
1085 bindings.allocate(indices);
1086 bindings.allocate(lengths);
1087
1088 auto *SLWS = F->createSparseLengthsWeightedSum("SLWS", data, weights, indices,
1089 lengths);
1090 auto *res = F->createSave("save", SLWS);
1091 ::glow::convertPlaceholdersToConstants(
1092 F, bindings, {indices, lengths, res->getPlaceholder()});
1093 bindings.allocate(res->getPlaceholder());
1094
1095 quantization::QuantizationConfiguration quantConfig{{
1096 {SLWS->getData().generateNodeOutputName(), {0.2f, 2.0f}},
1097 {SLWS->getWeights().generateNodeOutputName(), {0.3f, 3.0f}},
1098 {SLWS->getResult().generateNodeOutputName(), {0.4f, 4.0f}},
1099 }};
1100
1101 quantConfig.enableRowwise = true;
1102 quantConfig.assertAllNodesQuantized = true;
1103 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
1104 quantization::quantizeFunction(F, quantConfig, *backend);
1105 std::string saveName = std::string(res->getName());
1106 EE.compile(CompilationMode::Infer);
1107
1108 // Check the graph structure after quantization.
1109 F = EE.getModule().getFunctions().front();
1110 auto *saveNode = llvm::dyn_cast<SaveNode>(F->getNodeByName(saveName));
1111 ASSERT_TRUE(saveNode);
1112 auto *FRWQSLWS =
1113 llvm::dyn_cast<FusedRowwiseQuantizedSparseLengthsWeightedSumNode>(
1114 saveNode->getInput().getNode());
1115 ASSERT_TRUE(FRWQSLWS);
1116 }
1117
1118 /// Quantize ReLU node and make sure that quantized version
1119 /// has quantization parameters mapping to non-negative floating
1120 /// point range.
TEST(Quantization,quantizeReLU)1121 TEST(Quantization, quantizeReLU) {
1122 ExecutionEngine EE{};
1123 std::unique_ptr<Backend> backend(new MockQuantBackend);
1124 EE.setBackendName("Interpreter");
1125 auto &mod = EE.getModule();
1126 Function *F = mod.createFunction("main");
1127 auto *input = mod.createPlaceholder(ElemKind::FloatTy, {1, 3}, "input", true);
1128 auto *relu = F->createRELU("ReLU", input);
1129 PlaceholderBindings bindings;
1130 auto *ret = F->createSave("ret", relu);
1131 std::string retName = std::string(ret->getName());
1132 // Make sure that offset quantization parameter of ReLU is set
1133 // such that it produces non-negative floating point range.
1134 quantization::QuantizationConfiguration quantConfig{
1135 {{input->getOutput().generateNodeOutputName(), {0.2f, 2.0f}},
1136 {relu->getResult().generateNodeOutputName(), {0.0f, 3.0f}}}};
1137 quantConfig.assertAllNodesQuantized = true;
1138 quantization::quantizeFunction(F, quantConfig, *backend);
1139 EE.compile(CompilationMode::Infer);
1140
1141 // Compute tensor quantization parameters for verification.
1142 auto reluTQP = chooseQuantizationParams({0.0f, 3.0f}, quantConfig.schema,
1143 quantConfig.precision);
1144
1145 F = EE.getModule().getFunctions().front();
1146 auto *save = llvm::cast<SaveNode>(F->getNodeByName(retName));
1147 ASSERT_TRUE(llvm::isa<DequantizeNode>(save->getInput().getNode()));
1148 auto *dequantize = llvm::cast<DequantizeNode>(save->getInput().getNode());
1149 ASSERT_TRUE(llvm::isa<MaxNode>(dequantize->getInput().getNode()));
1150
1151 MaxNode *max = llvm::cast<MaxNode>(dequantize->getInput().getNode());
1152 ASSERT_TRUE(max->getResult().getType()->isQuantizedType());
1153 EXPECT_EQ(max->getResult().getType()->getOffset(), reluTQP.offset);
1154 EXPECT_EQ(max->getResult().getType()->getScale(), reluTQP.scale);
1155 }
1156
1157 /// Quantize Log, Sigmoid, and Tanh nodes and make sure that quantized versions
1158 /// are implemented as IntLookupTables, because the Interpreter only supports
1159 /// them as such.
TEST(Quantization,quantizeLookupTables)1160 TEST(Quantization, quantizeLookupTables) {
1161 ExecutionEngine EE{};
1162 auto &mod = EE.getModule();
1163 Function *F = mod.createFunction("main");
1164 auto *input = mod.createPlaceholder(ElemKind::FloatTy, {1, 3}, "input", true);
1165 auto *LN = F->createLog("log", input);
1166 auto *SN = F->createSigmoid("sigmoid", LN);
1167 auto *TN = F->createTanh("tanh", SN);
1168 auto *ret = F->createSave("ret", TN);
1169
1170 quantization::QuantizationConfiguration quantConfig{
1171 {{input->getOutput().generateNodeOutputName(), {0.2f, 2.0f}},
1172 {LN->getResult().generateNodeOutputName(LN->getName()), {0.3f, 3.0f}},
1173 {SN->getResult().generateNodeOutputName(), {0.4f, 4.0f}},
1174 {TN->getResult().generateNodeOutputName(), {0.5f, 5.0f}}}};
1175 quantConfig.assertAllNodesQuantized = true;
1176 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
1177 quantization::quantizeFunction(F, quantConfig, *backend);
1178 optimize(F, CompilationMode::Infer);
1179
1180 // Compute the quantization parameters based on the requirements of the
1181 // Sigmoid/Tanh or on the input/output values for Log.
1182 auto logInpTQP = chooseQuantizationParams({0.2, 2.0}, quantConfig.schema,
1183 quantConfig.precision);
1184 auto logOutTQP = chooseQuantizationParams({0.3, 3.0}, quantConfig.schema,
1185 quantConfig.precision);
1186 auto sigmoidInpTQP = chooseQuantizationParams({-6.0, 6.0}, quantConfig.schema,
1187 quantConfig.precision);
1188 auto sigmoidOutTQP = chooseQuantizationParams({0.0, 1.0}, quantConfig.schema,
1189 quantConfig.precision);
1190 auto tanhInpTQP = chooseQuantizationParams({-3.0, 3.0}, quantConfig.schema,
1191 quantConfig.precision);
1192 auto tanhOutTQP = chooseQuantizationParams({-1.0, 1.0}, quantConfig.schema,
1193 quantConfig.precision);
1194
1195 auto *save = llvm::cast<SaveNode>(F->getNodeByName(ret->getName()));
1196 auto *dequantizeTanh =
1197 llvm::dyn_cast<DequantizeNode>(save->getInput().getNode());
1198 ASSERT_TRUE(dequantizeTanh);
1199 auto *tanhILT =
1200 llvm::dyn_cast<IntLookupTableNode>(dequantizeTanh->getInput().getNode());
1201 ASSERT_TRUE(tanhILT);
1202 EXPECT_FLOAT_EQ(tanhILT->getResult().getType()->getScale(), tanhOutTQP.scale);
1203 EXPECT_EQ(tanhILT->getResult().getType()->getOffset(), tanhOutTQP.offset);
1204 EXPECT_FLOAT_EQ(tanhILT->getInput().getType()->getScale(), tanhInpTQP.scale);
1205 EXPECT_EQ(tanhILT->getInput().getType()->getOffset(), tanhInpTQP.offset);
1206
1207 auto *rescaleSigmoid =
1208 llvm::dyn_cast<RescaleQuantizedNode>(tanhILT->getInput().getNode());
1209 ASSERT_TRUE(rescaleSigmoid);
1210 auto *sigmoidILT =
1211 llvm::dyn_cast<IntLookupTableNode>(rescaleSigmoid->getInput().getNode());
1212 ASSERT_TRUE(sigmoidILT);
1213 EXPECT_FLOAT_EQ(sigmoidILT->getResult().getType()->getScale(),
1214 sigmoidOutTQP.scale);
1215 EXPECT_EQ(sigmoidILT->getResult().getType()->getOffset(),
1216 sigmoidOutTQP.offset);
1217 EXPECT_FLOAT_EQ(sigmoidILT->getInput().getType()->getScale(),
1218 sigmoidInpTQP.scale);
1219 EXPECT_EQ(sigmoidILT->getInput().getType()->getOffset(),
1220 sigmoidInpTQP.offset);
1221
1222 auto *rescaleLog =
1223 llvm::dyn_cast<RescaleQuantizedNode>(sigmoidILT->getInput().getNode());
1224 ASSERT_TRUE(rescaleLog);
1225 auto *logILT =
1226 llvm::dyn_cast<IntLookupTableNode>(rescaleLog->getInput().getNode());
1227 ASSERT_TRUE(logILT);
1228 EXPECT_FLOAT_EQ(logILT->getResult().getType()->getScale(), logOutTQP.scale);
1229 EXPECT_EQ(logILT->getResult().getType()->getOffset(), logOutTQP.offset);
1230 EXPECT_FLOAT_EQ(logILT->getInput().getType()->getScale(), logInpTQP.scale);
1231 EXPECT_EQ(logILT->getInput().getType()->getOffset(), logInpTQP.offset);
1232 }
1233
1234 /// Quantize Log, Sigmoid, and Tanh nodes and make sure that they are not
1235 /// replaced by LookupTables because the backend supports them directly.
TEST(Quantization,quantizeWithoutLookupTables)1236 TEST(Quantization, quantizeWithoutLookupTables) {
1237 ExecutionEngine EE{};
1238 std::unique_ptr<Backend> backend(new MockQuantBackend);
1239 EE.setBackendName("Interpreter");
1240 auto &mod = EE.getModule();
1241 Function *F = mod.createFunction("main");
1242 auto *input = mod.createPlaceholder(ElemKind::FloatTy, {1, 3}, "input", true);
1243 auto *LN = F->createLog("log", input);
1244 auto *SN = F->createSigmoid("sigmoid", LN);
1245 auto *TN = F->createTanh("tanh", SN);
1246 auto *ret = F->createSave("ret", TN);
1247
1248 quantization::QuantizationConfiguration quantConfig{
1249 {{input->getOutput().generateNodeOutputName(), {0.2f, 2.0f}},
1250 {LN->getResult().generateNodeOutputName(), {0.3f, 3.0f}},
1251 {SN->getResult().generateNodeOutputName(), {0.4f, 4.0f}},
1252 {TN->getResult().generateNodeOutputName(), {0.5f, 5.0f}}}};
1253 quantConfig.assertAllNodesQuantized = true;
1254 quantization::quantizeFunction(F, quantConfig, *backend);
1255 optimize(F, CompilationMode::Infer);
1256
1257 // Compute the quantization parameters for validation.
1258 auto logInpTQP = chooseQuantizationParams({0.2, 2.0}, quantConfig.schema,
1259 quantConfig.precision);
1260 auto logOutTQP = chooseQuantizationParams({0.3, 3.0}, quantConfig.schema,
1261 quantConfig.precision);
1262 auto sigmoidInpTQP = chooseQuantizationParams({0.3, 3.0}, quantConfig.schema,
1263 quantConfig.precision);
1264 auto sigmoidOutTQP = chooseQuantizationParams(
1265 {0.4f, 4.0f}, quantConfig.schema, quantConfig.precision);
1266 auto tanhInpTQP = chooseQuantizationParams({0.4f, 4.0f}, quantConfig.schema,
1267 quantConfig.precision);
1268 auto tanhOutTQP = chooseQuantizationParams({0.5f, 5.0f}, quantConfig.schema,
1269 quantConfig.precision);
1270
1271 auto *save = llvm::cast<SaveNode>(F->getNodeByName(ret->getName()));
1272 auto *dequantize = llvm::dyn_cast<DequantizeNode>(save->getInput().getNode());
1273 ASSERT_TRUE(dequantize);
1274 auto *tanh = llvm::dyn_cast<TanhNode>(dequantize->getInput());
1275 ASSERT_TRUE(tanh);
1276 EXPECT_FLOAT_EQ(tanh->getResult().getType()->getScale(), tanhOutTQP.scale);
1277 EXPECT_EQ(tanh->getResult().getType()->getOffset(), tanhOutTQP.offset);
1278 EXPECT_FLOAT_EQ(tanh->getInput().getType()->getScale(), tanhInpTQP.scale);
1279 EXPECT_EQ(tanh->getInput().getType()->getOffset(), tanhInpTQP.offset);
1280
1281 auto *sigmoid = llvm::dyn_cast<SigmoidNode>(tanh->getInput());
1282 ASSERT_TRUE(sigmoid);
1283 EXPECT_FLOAT_EQ(sigmoid->getResult().getType()->getScale(),
1284 sigmoidOutTQP.scale);
1285 EXPECT_EQ(sigmoid->getResult().getType()->getOffset(), sigmoidOutTQP.offset);
1286 EXPECT_FLOAT_EQ(sigmoid->getInput().getType()->getScale(),
1287 sigmoidInpTQP.scale);
1288 EXPECT_EQ(sigmoid->getInput().getType()->getOffset(), sigmoidInpTQP.offset);
1289
1290 auto *log = llvm::dyn_cast<LogNode>(sigmoid->getInput());
1291 ASSERT_TRUE(log);
1292 EXPECT_FLOAT_EQ(log->getResult().getType()->getScale(), logOutTQP.scale);
1293 EXPECT_EQ(log->getResult().getType()->getOffset(), logOutTQP.offset);
1294 EXPECT_FLOAT_EQ(log->getInput().getType()->getScale(), logInpTQP.scale);
1295 EXPECT_EQ(log->getInput().getType()->getOffset(), logInpTQP.offset);
1296 }
1297
1298 /// Fills the tensor \p H with some stable random data with the seed \p seed
1299 /// and the range [-scale .. scale].
fillStableRandomData(Handle<float> H,size_t seed,float scale=1)1300 static void fillStableRandomData(Handle<float> H, size_t seed,
1301 float scale = 1) {
1302 for (size_t i = 0, e = H.size(); i < e; i++) {
1303 H.raw(i) = scale * (float((int(i * 1921 + seed) % 100) - 50) / 50);
1304 }
1305 }
1306
1307 /// Builds a simple graph, returns back input var and save node through refs.
createSimpleGraphForQuantization(Module * M,PlaceholderBindings & bindings,Placeholder * A,Placeholder * B,llvm::StringRef funcName)1308 static Function *createSimpleGraphForQuantization(Module *M,
1309 PlaceholderBindings &bindings,
1310 Placeholder *A,
1311 Placeholder *B,
1312 llvm::StringRef funcName) {
1313 Function *F = M->createFunction(funcName);
1314
1315 fillStableRandomData(bindings.allocate(A)->getHandle(), 1100, 1);
1316
1317 fillStableRandomData(bindings.allocate(B)->getHandle(), 2001, 1);
1318
1319 ConvolutionNode *CV = F->createConv(bindings, "conv", A, 16, 5, 1, 2, 2);
1320 auto *bias = cast<Placeholder>(CV->getBias());
1321 auto *filter = cast<Placeholder>(CV->getFilter());
1322 fillStableRandomData(bindings.get(bias)->getHandle(), 2001, 1);
1323 fillStableRandomData(bindings.get(filter)->getHandle(), 1000, 1);
1324
1325 auto *RL = F->createRELU("relu", CV);
1326 auto *MP = F->createMaxPool("maxPool", RL, 2, 2, 1);
1327 // Just add noop transpose.
1328 auto *T = F->createTranspose("transpose", MP->getResult(), {0, 1, 2, 3});
1329 // Noop reshape, make sure conversion quantization procedure works well.
1330 auto *R = F->createReshape("reshape", T, T->getResult().dims());
1331 auto *AP = F->createAvgPool("avgPool", R, 2, 2, 1);
1332
1333 FullyConnectedNode *FC = F->createFullyConnected(bindings, "fc", AP, 10);
1334
1335 // Noop slice, make sure conversion quantization procedure works well.
1336 auto *S =
1337 F->createSlice("slice", FC, {0, 1},
1338 {FC->getResult().dims()[0], FC->getResult().dims()[1]});
1339 auto *bias2 = cast<Placeholder>(FC->getBias());
1340 auto *filter2 = cast<Placeholder>(FC->getWeights());
1341
1342 fillStableRandomData(bindings.get(bias2)->getHandle(), 3001, 1);
1343 fillStableRandomData(bindings.get(filter2)->getHandle(), 4000, 1);
1344
1345 auto *CN = F->createConcat("concat", {S, B}, 0);
1346 auto *SP = F->createSplat("splat", B->getType(), 10.0);
1347 auto *O = F->createConcat("concat", {CN, SP}, 0);
1348 auto *TN = F->createTranspose("transpose", O, {1, 0});
1349 auto *BRAN = F->createBatchedReduceAdd("batchedreduceadd", TN, 0);
1350 auto *TLN = F->createTile("tile", BRAN, 2, 0);
1351 auto *SN = F->createSplat("splat", TLN->getResult().getType(), 100.0);
1352 auto *MN = F->createMax("max", SN, TLN);
1353 auto *CLTE = F->createCmpLTE("cmplte", MN, SN);
1354 auto *SLN = F->createSelect("select", CLTE, SN, MN);
1355 auto *save = F->createSave("save", SLN);
1356 bindings.allocate(save->getPlaceholder());
1357 return F;
1358 }
1359
1360 /// Helper for an end to end test profiling a model on \p profileEE, then
1361 /// quantizing and running it on \p backendSpecificEE, quantizing with precision
1362 /// \p quantizationPrecision and disabling quantization for all Kinds in
1363 /// \p keepOriginalPrecisionForNodes. Results are compared from the profiling
1364 /// run and quantization run.
1365 static void
testQuantizationEnd2End(ExecutionEngine & profileEE,ExecutionEngine & backendSpecificEE,ElemKind quantizationPrecision,const KindSet & keepOriginalPrecisionForNodes={})1366 testQuantizationEnd2End(ExecutionEngine &profileEE,
1367 ExecutionEngine &backendSpecificEE,
1368 ElemKind quantizationPrecision,
1369 const KindSet &keepOriginalPrecisionForNodes = {}) {
1370 auto *mod = &profileEE.getModule();
1371 auto *modBackend = &backendSpecificEE.getModule();
1372 PlaceholderBindings bindings;
1373 PlaceholderBindings bindingsBackend;
1374
1375 auto *A =
1376 mod->createPlaceholder(ElemKind::FloatTy, {1, 32, 32, 2}, "A", false);
1377 auto *B = mod->createPlaceholder(ElemKind::FloatTy, {10, 9}, "B", false);
1378 auto *AB = modBackend->createPlaceholder(ElemKind::FloatTy, {1, 32, 32, 2},
1379 "A", false);
1380 auto *BB =
1381 modBackend->createPlaceholder(ElemKind::FloatTy, {10, 9}, "B", false);
1382
1383 // STEP1 - Generate the first network to record the quantization parameters.
1384 createSimpleGraphForQuantization(mod, bindings, A, B, "main");
1385 createSimpleGraphForQuantization(modBackend, bindingsBackend, AB, BB, "main");
1386
1387 LoweredInfoMap loweredMapForProf;
1388 CompilationContext cctxProf{&bindings, &loweredMapForProf};
1389 cctxProf.precisionConfig.quantMode = QuantizationMode::Profile;
1390 profileEE.compile(cctxProf);
1391 bindings.allocate(mod->getPlaceholders());
1392
1393 // Run graph to capture profile.
1394 profileEE.run(bindings, "main");
1395
1396 // STEP2 - Use the profile to quantize a network.
1397 LoweredInfoMap loweredMapForQuant;
1398 CompilationContext cctxQuant{&bindings, &loweredMapForQuant};
1399
1400 // Get quantization infos and build new quantized graph.
1401 PrecisionConfiguration &precConfig = cctxQuant.precisionConfig;
1402 precConfig.quantMode = QuantizationMode::Quantize;
1403 precConfig.quantConfig.infos = quantization::generateNodeProfilingInfos(
1404 bindings, mod->getFunctions().front(), loweredMapForProf);
1405 precConfig.quantConfig.precision = quantizationPrecision;
1406 precConfig.quantConfig.assertAllNodesQuantized = true;
1407 precConfig.precisionModeKindSet = keepOriginalPrecisionForNodes;
1408
1409 backendSpecificEE.compile(cctxQuant);
1410 bindingsBackend.allocate(modBackend->getPlaceholders());
1411 backendSpecificEE.run(bindingsBackend);
1412
1413 // STEP3 - Compare the results of the original and quantized functions.
1414 auto result1Handle =
1415 bindings.get(bindings.getPlaceholderByNameSlow("save"))->getHandle();
1416 auto result2Handle =
1417 bindingsBackend.get(bindingsBackend.getPlaceholderByNameSlow("save"))
1418 ->getHandle();
1419
1420 EXPECT_EQ(result1Handle.size(), result2Handle.size());
1421
1422 for (int i = 0, e = result1Handle.size(); i < e; ++i) {
1423 float mx = result2Handle.raw(result2Handle.minMaxArg().second);
1424 double diff = std::fabs(result2Handle.raw(i) - result1Handle.raw(i)) / mx;
1425
1426 // Allow 3% difference.
1427 EXPECT_NEAR(diff, 0, 0.03);
1428 }
1429 }
1430
1431 /// End to end quantization test for Int8 quantization.
TEST_P(Operator,end2endInt8)1432 TEST_P(Operator, end2endInt8) {
1433 // The OpenCL backend does not support some of the nodes in the test;
1434 // explicitly whitelist them here as staying in float, so that the quantizer
1435 // does not complain.
1436 KindSet keepOriginalPrecisionForNodes;
1437 if (backendSpecificEE.getBackendName() == "OpenCL") {
1438 keepOriginalPrecisionForNodes.insert(Kinded::Kind::SelectNodeKind);
1439 keepOriginalPrecisionForNodes.insert(Kinded::Kind::CmpLTENodeKind);
1440 keepOriginalPrecisionForNodes.insert(
1441 Kinded::Kind::BatchedReduceAddNodeKind);
1442 }
1443
1444 testQuantizationEnd2End(profileEE, backendSpecificEE, ElemKind::Int8QTy,
1445 keepOriginalPrecisionForNodes);
1446 }
1447
1448 /// Fills the tensor \p H with some stable random integers with the seed \p seed
1449 /// and the range [0, scale).
fillStableRandomIndex(Handle<int64_t> H,size_t seed,size_t scale=10)1450 static void fillStableRandomIndex(Handle<int64_t> H, size_t seed,
1451 size_t scale = 10) {
1452 for (size_t i = 0, e = H.size(); i < e; i++) {
1453 H.raw(i) = int(i * 1921 + seed) % scale;
1454 }
1455 }
1456
1457 /// Builds a graph with two GRUs and saves output from last hidden node.
createGRUForQuantization(Module * M,PlaceholderBindings & bindings,llvm::StringRef funcName)1458 static Function *createGRUForQuantization(Module *M,
1459 PlaceholderBindings &bindings,
1460 llvm::StringRef funcName) {
1461 Function *F = M->createFunction(funcName);
1462
1463 constexpr unsigned sequenceSize = 2;
1464 constexpr unsigned embeddingSize = 10;
1465 constexpr unsigned languageSize = 10;
1466 constexpr unsigned batchSize = 5;
1467 constexpr unsigned hiddenSize = 3 * embeddingSize;
1468
1469 // STEP1 - Initialize inputs into GRU
1470 auto *emb = F->getParent()->createPlaceholder(
1471 ElemKind::FloatTy, {languageSize, embeddingSize}, "embedding", false);
1472 fillStableRandomData(bindings.allocate(emb)->getHandle(), 4565, 1);
1473
1474 auto *input = F->getParent()->createPlaceholder(
1475 ElemKind::Int64ITy, {batchSize, sequenceSize}, "input", false);
1476 fillStableRandomIndex(bindings.allocate(input)->getHandle<int64_t>(), 7227,
1477 10);
1478
1479 auto *hiddenInit = F->getParent()->createPlaceholder(
1480 ElemKind::FloatTy, {batchSize, embeddingSize}, "hiddenInit", false);
1481 bindings.allocate(hiddenInit)->zero();
1482 Node *hidden = hiddenInit;
1483
1484 for (unsigned step = 0; step < sequenceSize; step++) {
1485 // STEP2 - Gather a single set of embeddings for the GRU
1486 Node *inputEmbedded = F->createGather("gru.embedding", emb, input);
1487 Node *inputSlice =
1488 F->createSlice("gru.inputSlice", inputEmbedded, {0, step, 0},
1489 {batchSize, step + 1, embeddingSize});
1490 Node *reshape =
1491 F->createReshape("gru.reshape", inputSlice, {batchSize, embeddingSize});
1492
1493 // STEP3 - Generate a GRU
1494 // reference implementation:
1495 // https://github.com/pytorch/pytorch/blob/dd5c195646b941d3e20a72847ac48c41e272b8b2/torch/nn/_functions/rnn.py#L46
1496 // similar to /examples/fr2en.cpp
1497
1498 auto *FCi =
1499 F->createFullyConnected(bindings, "gru.fci", reshape, hiddenSize);
1500 auto *biasI = cast<Placeholder>(FCi->getBias());
1501 auto *filterI = cast<Placeholder>(FCi->getWeights());
1502 fillStableRandomData(bindings.get(biasI)->getHandle(), 8877, 1);
1503 fillStableRandomData(bindings.get(filterI)->getHandle(), 1441, 1);
1504
1505 auto *FCh =
1506 F->createFullyConnected(bindings, "gru.fch", hidden, hiddenSize);
1507 auto *biasH = cast<Placeholder>(FCh->getBias());
1508 auto *filterH = cast<Placeholder>(FCh->getWeights());
1509 fillStableRandomData(bindings.get(biasH)->getHandle(), 9009, 1);
1510 fillStableRandomData(bindings.get(filterH)->getHandle(), 1001, 1);
1511
1512 Node *i_r =
1513 F->createSlice("gru.i_r", FCi, {0, 0}, {batchSize, embeddingSize});
1514 Node *i_i = F->createSlice("gru.i_i", FCi, {0, embeddingSize},
1515 {batchSize, 2 * embeddingSize});
1516 Node *i_n = F->createSlice("gru.i_n", FCi, {0, 2 * embeddingSize},
1517 {batchSize, 3 * embeddingSize});
1518
1519 Node *h_r =
1520 F->createSlice("gru.h_r", FCh, {0, 0}, {batchSize, embeddingSize});
1521 Node *h_i = F->createSlice("gru.h_i", FCh, {0, embeddingSize},
1522 {batchSize, 2 * embeddingSize});
1523 Node *h_n = F->createSlice("gru.h_n", FCh, {0, 2 * embeddingSize},
1524 {batchSize, 3 * embeddingSize});
1525
1526 Node *resetgate = F->createSigmoid("gru.resetgate",
1527 F->createAdd("i_r_plus_h_r", i_r, h_r));
1528 Node *inputgate = F->createSigmoid("gru.inputgate",
1529 F->createAdd("i_i_plus_h_i", i_i, h_i));
1530 Node *newgate = F->createTanh(
1531 "gru.newgate",
1532 F->createAdd("i_n_plus_rg_mult_h_n", i_n,
1533 F->createMul("rg_mult_h_n", resetgate, h_n)));
1534 hidden = F->createAdd(
1535 "gru.newhidden", newgate,
1536 F->createMul("ig_mult_hmng", inputgate,
1537 F->createSub("hidden_minus_newgate", hidden, newgate)));
1538 }
1539 // No-op TopK selection to test quantization
1540 Node *downsample = F->createTopK("gru.downsample", hidden, embeddingSize / 2);
1541
1542 auto *save = F->createSave("save", {downsample, 0});
1543 bindings.allocate(save->getPlaceholder());
1544 return F;
1545 }
1546
TEST_P(Operator,end2endGRU)1547 TEST_P(Operator, end2endGRU) {
1548 // STEP1 - Generate the first network to record the quantization parameters.
1549 auto *mod = &profileEE.getModule();
1550 auto *modBackend = &backendSpecificEE.getModule();
1551 PlaceholderBindings bindings;
1552 PlaceholderBindings bindingsBackend;
1553 createGRUForQuantization(mod, bindings, "main");
1554 createGRUForQuantization(modBackend, bindingsBackend, "main");
1555
1556 LoweredInfoMap loweredMapForProf;
1557 CompilationContext cctxProf{&bindings, &loweredMapForProf};
1558 cctxProf.precisionConfig.quantMode = QuantizationMode::Profile;
1559 profileEE.compile(cctxProf);
1560
1561 // Run graph to capture profile.
1562 profileEE.run(bindings);
1563
1564 LoweredInfoMap loweredMapForQuant;
1565 CompilationContext cctxQuant{&bindings, &loweredMapForQuant};
1566 cctxQuant.precisionConfig.quantMode = QuantizationMode::Quantize;
1567 PrecisionConfiguration &precConfig = cctxQuant.precisionConfig;
1568 precConfig.quantConfig.infos = quantization::generateNodeProfilingInfos(
1569 bindings, mod->getFunctions().front(), loweredMapForProf);
1570
1571 // The OpenCL backend does not support some of the nodes in the test;
1572 // explicitly whitelist them here as staying in float, so that the quantizer
1573 // does not complain.
1574 KindSet doNotQuantizeKinds;
1575 if (backendSpecificEE.getBackendName() == "OpenCL") {
1576 precConfig.precisionModeKindSet.insert(Kinded::Kind::TanhNodeKind);
1577 precConfig.precisionModeKindSet.insert(Kinded::Kind::SigmoidNodeKind);
1578 precConfig.precisionModeKindSet.insert(Kinded::Kind::GatherNodeKind);
1579 }
1580
1581 // STEP2 - Use the profile to quantize a network.
1582
1583 backendSpecificEE.compile(cctxQuant);
1584 backendSpecificEE.run(bindingsBackend);
1585
1586 // STEP3 - Compare the results of the original and quantized functions.
1587 auto result1Handle =
1588 bindings.get(bindings.getPlaceholderByNameSlow("save"))->getHandle();
1589 auto result2Handle =
1590 bindingsBackend.get(bindingsBackend.getPlaceholderByNameSlow("save"))
1591 ->getHandle();
1592
1593 EXPECT_EQ(result1Handle.size(), result2Handle.size());
1594
1595 for (int i = 0, e = result1Handle.size(); i < e; ++i) {
1596 float mx = result2Handle.raw(result2Handle.minMaxArg().second);
1597 double diff = std::fabs(result2Handle.raw(i) - result1Handle.raw(i)) / mx;
1598
1599 // Allow 3% difference.
1600 EXPECT_NEAR(diff, 0, 0.03);
1601 }
1602 }
1603
TEST(Quantization,rescaleSameType)1604 TEST(Quantization, rescaleSameType) {
1605 ExecutionEngine EE{};
1606 PlaceholderBindings bindings;
1607 auto &mod = EE.getModule();
1608 auto *F = mod.createFunction("foo");
1609 auto *input =
1610 mod.createPlaceholder(ElemKind::Int8QTy, {1, 1}, 0.5, 11, "input", true);
1611 bindings.allocate(input)->init(Tensor::InitKind::Broadcast, 21,
1612 mod.getPRNG());
1613
1614 auto *Q = F->createRescaleQuantized(
1615 "rescale", input, mod.uniqueType(ElemKind::Int8QTy, {1, 1}, 0.5, 11));
1616 auto *D = F->createDequantize("dequantize", Q, ElemKind::FloatTy);
1617 auto *save = F->createSave("ret", D);
1618 auto *result = bindings.allocate(save->getPlaceholder());
1619
1620 EXPECT_EQ(F->getNodes().size(), 3);
1621 EE.compile(CompilationMode::Infer);
1622
1623 EE.run(bindings);
1624 F = EE.getModule().getFunctions().front();
1625 EXPECT_EQ(F->getNodes().size(), 2);
1626
1627 auto RH = result->getHandle();
1628 EXPECT_NEAR(RH.at({0, 0}), 5.0, 0.001);
1629 }
1630
TEST(Quantization,optimizeRescaleQuantize)1631 TEST(Quantization, optimizeRescaleQuantize) {
1632 ExecutionEngine EE{};
1633 PlaceholderBindings bindings;
1634 auto &mod = EE.getModule();
1635 auto *F = mod.createFunction("foo");
1636 auto *input = mod.createPlaceholder(ElemKind::FloatTy, {1, 1}, "input", true);
1637 bindings.allocate(input)->init(Tensor::InitKind::Broadcast, 21,
1638 mod.getPRNG());
1639
1640 auto *Q = F->createQuantize(
1641 "quant", input, mod.uniqueType(ElemKind::Int8QTy, {1, 1}, 0.25, 4));
1642 auto *RS = F->createRescaleQuantized(
1643 "rescale", Q, mod.uniqueType(ElemKind::Int8QTy, {1, 1}, 0.5, 11));
1644 auto *D = F->createDequantize("dequantize", RS, ElemKind::FloatTy);
1645 auto *save = F->createSave("ret", D);
1646 auto *result = bindings.allocate(save->getPlaceholder());
1647
1648 EXPECT_EQ(F->getNodes().size(), 4);
1649 EE.compile(CompilationMode::Infer);
1650
1651 EE.run(bindings);
1652
1653 EXPECT_EQ(EE.getModule().getFunctions().front()->getNodes().size(), 1);
1654
1655 auto RH = result->getHandle();
1656 EXPECT_NEAR(RH.at({0, 0}), 21.0, 0.001);
1657 }
1658
1659 /// Check that our asymmetric quantization schema produces
1660 /// the expected scales and offsets for various ranges.
TEST(Quantization,chooseQuantizationAsymmetric)1661 TEST(Quantization, chooseQuantizationAsymmetric) {
1662 // Map float [0.0; 6.0] to int [-128; 127].
1663 TensorQuantizationParams asymmetricParams =
1664 chooseQuantizationParams({0.0, 6.0}, quantization::Schema::Asymmetric);
1665 // Dequantization formula is scale(X - offset).
1666 // So
1667 // 1. scale(-128 - offset) == 0.0
1668 // 2. scale(127 - offset) == 6.0
1669 // Given scale != 0, #1 gives -128 == offset
1670 // Then #2, gives scale == 6.0 / (127 - (-128)).
1671 EXPECT_EQ(asymmetricParams.offset, -128);
1672 EXPECT_NEAR(asymmetricParams.scale, 6.0 / 255, 0.001);
1673
1674 // Map float [-3.0; 3.0] to int [-128; 127].
1675 asymmetricParams =
1676 chooseQuantizationParams({-3.0, 3.0}, quantization::Schema::Asymmetric);
1677 // Dequantization formula is scale(X - offset).
1678 // So in theory, we should get
1679 // 1. scale(-128 - offset) == -3.0
1680 // 2. scale(127 - offset) == 3.0
1681 // Given scale != 0, #1 + #2 gives scale(-128 + 127 - 2*offset) == 0.0
1682 // offset == -1 / -2 == 0.5
1683 // Then #2 or #1, gives scale == 3.0 / 127.5.
1684 // However, when we get symmetric ranges (i.e., [-X; X]),
1685 // we actually force the zero point to map to 0.
1686 // In other words, scale(0 - offset) == 0.0, so our offset is 0.
1687 // Then our scale is simply: (inputMax - inputMin) / (outputMax - outputMin).
1688 // (3.0 - (-3.0)) / (127 - (-128)) == 6.0 / 255.
1689 EXPECT_EQ(asymmetricParams.offset, 0);
1690 EXPECT_NEAR(asymmetricParams.scale, 6.0 / 255, 0.001);
1691
1692 // Map float [-2.0; 5.0] to int [-128; 127].
1693 asymmetricParams =
1694 chooseQuantizationParams({-2.0, 5.0}, quantization::Schema::Asymmetric);
1695 // Scale: (5.0 - (-2.0)) / (127 - (-128)) == 7.0 / 255.0
1696 // Offset from min: scale(-128 - offset) == -2.0
1697 // 7.0 / 255.0 * (-128 - offset) == -2.0
1698 // -128 - offset == -2.0 * 255.0 / 7.0
1699 // offset == 2.0 * 255.0 / 7.0 - 128
1700 // offset == ~-55
1701 EXPECT_EQ(asymmetricParams.offset, (int32_t)(2.0 * 255 / 7.0 - 128));
1702 EXPECT_NEAR(asymmetricParams.scale, 7.0 / 255, 0.001);
1703
1704 // Map float [2.0; 5.0] to int [-128; 127].
1705 // Make sure we extend the range to include 0.0, i.e.,
1706 // we really map [0.0; 5.0] to int [-128; 127].
1707 asymmetricParams =
1708 chooseQuantizationParams({2.0, 5.0}, quantization::Schema::Asymmetric);
1709 // Scale: (5.0 - (0.0)) / (127 - (-128)) == 5.0 / 255.0
1710 // Offset from min: scale(-128 - offset) == 0.0
1711 EXPECT_EQ(asymmetricParams.offset, -128);
1712 EXPECT_NEAR(asymmetricParams.scale, 5.0 / 255, 0.001);
1713
1714 // Map float [-8.0; -2.0] to int [-128; 127].
1715 // Make sure we extend the range to include 0.0, i.e.,
1716 // we really map [-8.0; 0.0] to int [-128; 127].
1717 asymmetricParams =
1718 chooseQuantizationParams({-8.0, -2.0}, quantization::Schema::Asymmetric);
1719 // Scale: (0.0 - (-8.0)) / (127 - (-128)) == 8.0 / 255.0
1720 // Offset from min: scale(127 - offset) == 0.0
1721 EXPECT_EQ(asymmetricParams.offset, 127);
1722 EXPECT_NEAR(asymmetricParams.scale, 8.0 / 255, 0.001);
1723 }
1724
1725 /// Check that our symmetric quantization schema produces
1726 /// the expected scales and offsets for various ranges.
TEST(Quantization,chooseQuantizationSymmetric)1727 TEST(Quantization, chooseQuantizationSymmetric) {
1728 // Map float [0.0; 6.0] to int [-128; 127].
1729 // With symmetric mapping, we basically map [-6.0; 6.0]
1730 TensorQuantizationParams symmetricParams =
1731 chooseQuantizationParams({0.0, 6.0}, quantization::Schema::Symmetric);
1732 // With symmetric mapping offset should always be zero.
1733 EXPECT_EQ(symmetricParams.offset, 0);
1734 EXPECT_NEAR(symmetricParams.scale, 12.0 / 255, 0.001);
1735
1736 // Map float [-3.0; 3.0] to int [-128; 127].
1737 symmetricParams =
1738 chooseQuantizationParams({-3.0, 3.0}, quantization::Schema::Symmetric);
1739 EXPECT_EQ(symmetricParams.offset, 0);
1740 EXPECT_NEAR(symmetricParams.scale, 6.0 / 255, 0.001);
1741
1742 // Map float [-2.0; 5.0] to int [-128; 127].
1743 // => [-5.0; 5.0] range for symmetric mode.
1744 symmetricParams =
1745 chooseQuantizationParams({-2.0, 5.0}, quantization::Schema::Symmetric);
1746 EXPECT_EQ(symmetricParams.offset, 0);
1747 EXPECT_NEAR(symmetricParams.scale, 10.0 / 255, 0.001);
1748
1749 // Map float [2.0; 5.0] to int [-128; 127].
1750 // Ranges are extended to include 0.
1751 // => [0.0; 5.0] range for symmetric mode.
1752 symmetricParams =
1753 chooseQuantizationParams({2.0, 5.0}, quantization::Schema::Symmetric);
1754 // Scale: (5.0 - (0.0)) / (127 - (-128)) == 5.0 / 255.0
1755 // Offset from min: scale(-128 - offset) == 0.0
1756 EXPECT_EQ(symmetricParams.offset, 0);
1757 EXPECT_NEAR(symmetricParams.scale, 10.0 / 255, 0.001);
1758
1759 // Map float [-8.0; -2.0] to int [-128; 127].
1760 // => [-8.0; 8.0] range for symmetric mode.
1761 symmetricParams =
1762 chooseQuantizationParams({-8.0, -2.0}, quantization::Schema::Symmetric);
1763 EXPECT_EQ(symmetricParams.offset, 0);
1764 EXPECT_NEAR(symmetricParams.scale, 16.0 / 255, 0.001);
1765 }
1766
1767 /// Check quantization symmetry in presence of infinities.
TEST(Quantization,chooseQuantizationSymmetricInf)1768 TEST(Quantization, chooseQuantizationSymmetricInf) {
1769 auto sym = quantization::Schema::Symmetric;
1770 EXPECT_EQ(chooseQuantizationParams({-INFINITY, INFINITY}, sym).offset, 0);
1771 EXPECT_EQ(chooseQuantizationParams({INFINITY, INFINITY}, sym).offset, 0);
1772 EXPECT_EQ(chooseQuantizationParams({-INFINITY, -INFINITY}, sym).offset, 0);
1773 EXPECT_EQ(chooseQuantizationParams({-INFINITY, 1.0f}, sym).offset, 0);
1774 EXPECT_EQ(chooseQuantizationParams({-INFINITY, -1.0f}, sym).offset, 0);
1775 EXPECT_EQ(chooseQuantizationParams({-1.0f, INFINITY}, sym).offset, 0);
1776 EXPECT_EQ(chooseQuantizationParams({1.0f, INFINITY}, sym).offset, 0);
1777 }
1778
1779 /// Check that Relu can use our symmetric quantization schema.
TEST(Quantization,reluCanUseSymmetricSchema)1780 TEST(Quantization, reluCanUseSymmetricSchema) {
1781 PlaceholderBindings bindings;
1782 ExecutionEngine EE{};
1783 auto &mod = EE.getModule();
1784 Function *F = mod.createFunction("main");
1785
1786 Placeholder *input =
1787 mod.createPlaceholder(ElemKind::FloatTy, {10}, "input", false);
1788 auto *inputTensor = bindings.allocate(input);
1789 auto IH = inputTensor->getHandle<float>();
1790 for (dim_t i = 0; i < 10; i++) {
1791 IH.at({i}) = (i % 2 == 0) ? 5 : -5;
1792 }
1793
1794 // Create symmetric params that will be used for Relu.
1795 TensorQuantizationParams reluParams =
1796 chooseQuantizationParams({0.0, 10.0}, quantization::Schema::Symmetric);
1797 TypeRef reluTy = mod.uniqueType(ElemKind::Int8QTy, {10}, reluParams.scale,
1798 reluParams.offset);
1799 TensorQuantizationParams inputParams =
1800 chooseQuantizationParams({-10.0, 10.0}, quantization::Schema::Symmetric);
1801
1802 QuantizeNode *QN =
1803 F->createQuantize("quant", input,
1804 mod.uniqueType(ElemKind::Int8QTy, {10},
1805 inputParams.scale, inputParams.offset));
1806 ReluNode *RN = F->createRELU("relu", QN, reluTy);
1807 DequantizeNode *DN = F->createDequantize("dequantize", RN, ElemKind::FloatTy);
1808 SaveNode *SN = F->createSave("save", DN);
1809 auto *res = bindings.allocate(SN->getPlaceholder());
1810
1811 EE.compile(CompilationMode::Infer);
1812 EE.run(bindings);
1813
1814 // Verify all negative values were correctly set to zero.
1815 auto RH = res->getHandle();
1816 for (dim_t i = 0; i < 10; i++) {
1817 if (i % 2 == 0) {
1818 EXPECT_NEAR(RH.at({i}), 5, 0.05);
1819 } else {
1820 EXPECT_EQ(RH.at({i}), 0);
1821 }
1822 }
1823 }
1824
1825 /// Check that our symmetric with uint8 quantization schema produces
1826 /// the expected scales and offsets for various ranges.
TEST(Quantization,chooseQuantizationSymmetricWithUInt8)1827 TEST(Quantization, chooseQuantizationSymmetricWithUInt8) {
1828 // Map float [0.0; 6.0] to int [-128; 127].
1829 // With symmetric with uint8 mapping, we basically map [0.0; 6.0]
1830 TensorQuantizationParams symmetricParams = chooseQuantizationParams(
1831 {0.0, 6.0}, quantization::Schema::SymmetricWithUnsigned);
1832 // Given this is a purely positive range, we should use uint8,
1833 // thus int8 - (-128).
1834 EXPECT_EQ(symmetricParams.offset, -128);
1835 EXPECT_NEAR(symmetricParams.scale, 6.0 / 255, 0.001);
1836
1837 // Map float [-3.0; 3.0] to int [-128; 127].
1838 symmetricParams = chooseQuantizationParams(
1839 {-3.0, 3.0}, quantization::Schema::SymmetricWithUnsigned);
1840 EXPECT_EQ(symmetricParams.offset, 0);
1841 EXPECT_NEAR(symmetricParams.scale, 6.0 / 255, 0.001);
1842
1843 // Map float [-2.0; 5.0] to int [-128; 127].
1844 // This has negative value, thus we fall back to purely symmetric.
1845 // => [-5.0; 5.0] range for symmetric mode.
1846 symmetricParams = chooseQuantizationParams(
1847 {-2.0, 5.0}, quantization::Schema::SymmetricWithUnsigned);
1848 EXPECT_EQ(symmetricParams.offset, 0);
1849 EXPECT_NEAR(symmetricParams.scale, 10.0 / 255, 0.001);
1850
1851 // Map float [0; 0] to int [-128; 127].
1852 symmetricParams = chooseQuantizationParams(
1853 {0.0, 0.0}, quantization::Schema::SymmetricWithUnsigned);
1854 EXPECT_EQ(symmetricParams.offset, 0);
1855 EXPECT_NEAR(symmetricParams.scale, 0.1, 0.001);
1856
1857 // Map float [2.0; 5.0] to int [-128; 127].
1858 // All positive, using uint8.
1859 // However, our quantization schemas always include zero.
1860 // => [0.0; 5.0] range for uint8 mode.
1861 symmetricParams = chooseQuantizationParams(
1862 {2.0, 5.0}, quantization::Schema::SymmetricWithUnsigned);
1863 // Scale: (5.0 - (0.0)) / (127 - (-128)) == 5.0 / 255.0
1864 // Offset from min: scale(-128 - offset) == 0.0
1865 EXPECT_EQ(symmetricParams.offset, -128);
1866 EXPECT_NEAR(symmetricParams.scale, 5.0 / 255, 0.001);
1867
1868 // Map float [-8.0; -2.0] to int [-128; 127].
1869 // => [-8.0; 8.0] range for symmetric mode.
1870 symmetricParams = chooseQuantizationParams(
1871 {-8.0, -2.0}, quantization::Schema::SymmetricWithUnsigned);
1872 EXPECT_EQ(symmetricParams.offset, 0);
1873 EXPECT_NEAR(symmetricParams.scale, 16.0 / 255, 0.001);
1874 }
1875
1876 /// Verify the SymmetricWithPower2Scale quantization schema.
chooseQuantParamsPower2Scale(float min,float max,ElemKind qTy)1877 static void chooseQuantParamsPower2Scale(float min, float max, ElemKind qTy) {
1878 auto quantParams = quantization::chooseQuantizationParams(
1879 {min, max}, quantization::Schema::SymmetricWithPower2Scale, qTy);
1880 EXPECT_EQ(quantParams.offset, 0);
1881 EXPECT_TRUE(quantization::isFloatPowerOf2(quantParams.scale));
1882 }
1883
TEST(Quantization,chooseQuantizationSymmetricWithPower2Scale)1884 TEST(Quantization, chooseQuantizationSymmetricWithPower2Scale) {
1885 chooseQuantParamsPower2Scale(-3.0, 6.0, ElemKind::Int8QTy);
1886 chooseQuantParamsPower2Scale(3.0, 6.0, ElemKind::Int16QTy);
1887 chooseQuantParamsPower2Scale(-6.0, 0.0, ElemKind::Int32QTy);
1888 }
1889
1890 /// Check that LRN and Softmax are quantized.
TEST(Quantization,quantizeSoftmaxAndLRN)1891 TEST(Quantization, quantizeSoftmaxAndLRN) {
1892 ExecutionEngine EE{};
1893 PlaceholderBindings bindings;
1894 std::unique_ptr<Backend> backend(new MockQuantBackend);
1895 EE.setBackendName("Interpreter");
1896
1897 auto &mod = EE.getModule();
1898 Function *F = mod.createFunction("main");
1899
1900 auto *input =
1901 mod.createPlaceholder(ElemKind::FloatTy, {1, 10}, "input", true);
1902 auto *selected =
1903 mod.createPlaceholder(ElemKind::Int64ITy, {1, 10}, "selected", true);
1904 auto *LRN =
1905 F->createLocalResponseNormalization("LRN", input, 2, 1.0, 0.0001, 0.75);
1906 auto *SM = F->createSoftMax("softmax", LRN, selected);
1907 auto *SN = F->createSave("ret", SM);
1908
1909 quantization::QuantizationConfiguration quantConfig{
1910 {{input->getOutput().generateNodeOutputName(), {0.2f, 2.0f}},
1911 {LRN->getResult().generateNodeOutputName(LRN->getName()), {0.3f, 3.0f}},
1912 {SM->getResult().generateNodeOutputName(SM->getName()), {0.4f, 4.0f}},
1913 {NodeValue::generateNodeOutputName(SN->getName()), {0.4f, 4.0f}}}};
1914
1915 quantConfig.assertAllNodesQuantized = true;
1916 quantization::quantizeFunction(F, quantConfig, *backend);
1917
1918 auto qLRNIt = std::find_if(
1919 F->getNodes().begin(), F->getNodes().end(), [](const Node &node) -> bool {
1920 return llvm::isa<LocalResponseNormalizationNode>(&node) &&
1921 node.getNthResult(LocalResponseNormalizationNode::ResultIdx)
1922 .getType()
1923 ->isQuantizedType();
1924 });
1925 ASSERT_NE(qLRNIt, F->getNodes().end());
1926 auto qSMIt = std::find_if(F->getNodes().begin(), F->getNodes().end(),
1927 [](const Node &node) -> bool {
1928 return llvm::isa<SoftMaxNode>(&node) &&
1929 node.getNthResult(SoftMaxNode::ResultIdx)
1930 .getType()
1931 ->isQuantizedType();
1932 });
1933 ASSERT_NE(qSMIt, F->getNodes().end());
1934
1935 // Make sure that SaveNode is not quantized.
1936 for (const auto &node : F->getNodes()) {
1937 if (auto *saveNode = llvm::dyn_cast<SaveNode>(&node)) {
1938 EXPECT_FALSE(saveNode->getInput().getType()->isQuantizedType());
1939 }
1940 }
1941 }
1942
1943 /// Check that Select is quantized.
TEST(Quantization,quantizeSelect)1944 TEST(Quantization, quantizeSelect) {
1945 ExecutionEngine EE{};
1946 PlaceholderBindings bindings;
1947 std::unique_ptr<Backend> backend(new MockQuantBackend);
1948 EE.setBackendName("Interpreter");
1949
1950 auto &mod = EE.getModule();
1951 Function *F = mod.createFunction("main");
1952
1953 auto *LHS = mod.createPlaceholder(ElemKind::FloatTy, {1, 10}, "LHS", false);
1954 auto *RHS = mod.createPlaceholder(ElemKind::FloatTy, {1, 10}, "RHS", false);
1955 auto *cond = mod.createPlaceholder(ElemKind::BoolTy, {1, 10}, "cond", false);
1956 auto *select = F->createSelect("select", cond, LHS, RHS);
1957 F->createSave("save", select);
1958
1959 TensorProfilingParams LHSPP = {0.0, 1.0};
1960 TensorProfilingParams RHSPP = {-1.3, 2.7};
1961 TensorProfilingParams selectPP = {-2, 3.1};
1962
1963 quantization::QuantizationConfiguration quantConfig{
1964 {{LHS->getOutput().generateNodeOutputName(), LHSPP},
1965 {RHS->getOutput().generateNodeOutputName(), RHSPP},
1966 {select->getResult().generateNodeOutputName(), selectPP}}};
1967
1968 quantConfig.assertAllNodesQuantized = true;
1969 quantization::quantizeFunction(F, quantConfig, *backend);
1970
1971 // Get quantization parameters for verification.
1972 TensorQuantizationParams LHSQP = chooseQuantizationParams(
1973 LHSPP, quantConfig.schema, quantConfig.precision);
1974 TensorQuantizationParams RHSQP = chooseQuantizationParams(
1975 RHSPP, quantConfig.schema, quantConfig.precision);
1976 TensorQuantizationParams selectQP = chooseQuantizationParams(
1977 selectPP, quantConfig.schema, quantConfig.precision);
1978
1979 auto it = std::find_if(
1980 F->getNodes().begin(), F->getNodes().end(),
1981 [](const Node &node) -> bool { return llvm::isa<SelectNode>(&node); });
1982 ASSERT_NE(it, F->getNodes().end());
1983
1984 SelectNode *qSelect = llvm::cast<SelectNode>(&(*it));
1985 TypeRef qSelectTy = qSelect->getResult().getType();
1986 TypeRef qLHSTy = qSelect->getLHS().getType();
1987 TypeRef qRHSTy = qSelect->getRHS().getType();
1988
1989 ASSERT_TRUE(qSelectTy->isQuantizedType());
1990 EXPECT_EQ(qSelectTy->getScale(), selectQP.scale);
1991 EXPECT_EQ(qSelectTy->getOffset(), selectQP.offset);
1992 EXPECT_EQ(qLHSTy->getScale(), LHSQP.scale);
1993 EXPECT_EQ(qLHSTy->getOffset(), LHSQP.offset);
1994 EXPECT_EQ(qRHSTy->getScale(), RHSQP.scale);
1995 EXPECT_EQ(qRHSTy->getOffset(), RHSQP.offset);
1996 }
1997
1998 /// Check that AvgPool is quantized, and its input and output have different
1999 /// scale and offset.
TEST(Quantization,quantizeAvgPool)2000 TEST(Quantization, quantizeAvgPool) {
2001 ExecutionEngine EE{};
2002 PlaceholderBindings bindings;
2003 std::unique_ptr<Backend> backend(new MockQuantBackend);
2004 EE.setBackendName("Interpreter");
2005
2006 auto &mod = EE.getModule();
2007 Function *F = mod.createFunction("main");
2008
2009 auto *input =
2010 mod.createPlaceholder(ElemKind::FloatTy, {1, 3, 3, 1}, "input", true);
2011 auto *pool = F->createAvgPool("pool", input, {2, 2}, {1, 1}, {0, 0, 0, 0});
2012 auto *s = F->createSave("save", pool);
2013
2014 quantization::QuantizationConfiguration quantConfig{{
2015 {input->getOutput().generateNodeOutputName(), {-2.0f, 2.0f}},
2016 {pool->getResult().generateNodeOutputName(), {0.3f, 3.0f}},
2017 {NodeValue::generateNodeOutputName(s->getName()), {0.4f, 4.0f}},
2018 }};
2019
2020 quantConfig.assertAllNodesQuantized = true;
2021 quantization::quantizeFunction(F, quantConfig, *backend);
2022
2023 auto qPool = std::find_if(F->getNodes().begin(), F->getNodes().end(),
2024 [](const Node &node) -> bool {
2025 return llvm::isa<AvgPoolNode>(&node) &&
2026 node.getNthResult(AvgPoolNode::ResultIdx)
2027 .getType()
2028 ->isQuantizedType();
2029 });
2030 ASSERT_NE(qPool, F->getNodes().end());
2031 auto *avgPool = llvm::cast<AvgPoolNode>(qPool);
2032 ASSERT_NE(avgPool->getInput().getType()->getScale(),
2033 avgPool->getResult().getType()->getScale());
2034 ASSERT_NE(avgPool->getInput().getType()->getOffset(),
2035 avgPool->getResult().getType()->getOffset());
2036 }
2037
2038 /// Test option to disable quantization of specific node kinds in the graph.
TEST(Quantization,quantizeGraphPartially)2039 TEST(Quantization, quantizeGraphPartially) {
2040 ExecutionEngine EE{};
2041 PlaceholderBindings bindings;
2042 auto &mod = EE.getModule();
2043 Function *F = mod.createFunction("main");
2044
2045 auto *LHS = mod.createPlaceholder(ElemKind::FloatTy, {3, 3}, "lhs", true);
2046 auto *RHS = mod.createPlaceholder(ElemKind::FloatTy, {3, 3}, "rhs", true);
2047 bindings.allocate(LHS)->init(Tensor::InitKind::Xavier, 3, mod.getPRNG());
2048 bindings.allocate(RHS)->init(Tensor::InitKind::Xavier, 3, mod.getPRNG());
2049
2050 auto *MMN = F->createMatMul("matmul", LHS, RHS);
2051 auto *TN = F->createTanh("tanh", MMN);
2052 auto *save = F->createSave("ret", TN);
2053 auto *result = save->getPlaceholder();
2054 bindings.allocate(result);
2055
2056 // Note that we are creating profiling info even for nodes that will not be
2057 // quantized. This is how we expect quantizeFunction() to behave, as
2058 // quantization profiling will still get a profile for these nodes.
2059 quantization::QuantizationConfiguration quantConfig{{
2060 {LHS->getOutput().generateNodeOutputName(), {0.3f, 3.0f}},
2061 {RHS->getOutput().generateNodeOutputName(), {0.4f, 4.0f}},
2062 {MMN->getResult().generateNodeOutputName(), {0.6f, 6.0f}},
2063 {TN->getResult().generateNodeOutputName(), {0.5f, 5.0f}},
2064 }};
2065
2066 // Do not quantize any tanh nodes.
2067 KindSet doNotQuantizeKinds;
2068 doNotQuantizeKinds.insert(Kinded::Kind::TanhNodeKind);
2069
2070 quantConfig.assertAllNodesQuantized = true;
2071 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
2072 quantization::quantizeFunction(F, quantConfig, *backend,
2073 /* loweredMap */ {}, doNotQuantizeKinds);
2074
2075 // Make sure that graph can be compiled and run.
2076 ::glow::convertPlaceholdersToConstants(F, bindings, {result});
2077
2078 CompilationContext cctx;
2079 cctx.compMode = CompilationMode::Infer;
2080 // Do not perform any compile-time constant folding.
2081 cctx.optimizationOpts.enableConstantFolding = false;
2082 EE.compile(cctx);
2083
2084 EE.run(bindings);
2085
2086 {
2087 // Verify that the output variable is not quantized, and that it has a
2088 // single save node writer, which is also not quantized.
2089 EXPECT_TRUE(!result->getType()->isQuantizedType());
2090 ASSERT_EQ(result->getUsers().size(), 1);
2091 auto *SN = llvm::dyn_cast<SaveNode>(result->getUsers().begin()->getUser());
2092 ASSERT_TRUE(SN);
2093 EXPECT_TRUE(!SN->getOutput().getType()->isQuantizedType());
2094
2095 // Verify that the tanh is not quantized.
2096 auto *TN = llvm::dyn_cast<TanhNode>(SN->getInput());
2097 ASSERT_TRUE(TN);
2098 EXPECT_TRUE(!TN->getResult().getType()->isQuantizedType());
2099
2100 // Verify that the input to the tanh is a dequantize node.
2101 auto *DN = llvm::dyn_cast<DequantizeNode>(TN->getInput());
2102 ASSERT_TRUE(DN);
2103
2104 // Verify that the matmul is quantized.
2105 auto *MMN = llvm::dyn_cast<MatMulNode>(DN->getInput());
2106 ASSERT_TRUE(MMN);
2107 EXPECT_TRUE(MMN->getResult().getType()->isQuantizedType());
2108
2109 // Verify that the variable inputs to the matmul are quantized.
2110 auto *LHS = llvm::dyn_cast<Constant>(MMN->getLHS());
2111 ASSERT_TRUE(LHS);
2112 EXPECT_TRUE(LHS->getType()->isQuantizedType());
2113
2114 auto *RHS = llvm::dyn_cast<Constant>(MMN->getRHS());
2115 ASSERT_TRUE(RHS);
2116 EXPECT_TRUE(RHS->getType()->isQuantizedType());
2117 }
2118 }
2119
2120 /// Test option to disable quantization of specific node kinds in the graph,
2121 /// where there are multiple of that node kind.
TEST(Quantization,quantizeGraphPartiallyMultipleNodes)2122 TEST(Quantization, quantizeGraphPartiallyMultipleNodes) {
2123 ExecutionEngine EE{};
2124 PlaceholderBindings bindings;
2125 auto &mod = EE.getModule();
2126 Function *F = mod.createFunction("main");
2127
2128 auto *LHS = mod.createPlaceholder(ElemKind::FloatTy, {3, 3}, "lhs", true);
2129 auto *RHS = mod.createPlaceholder(ElemKind::FloatTy, {3, 3}, "rhs", true);
2130 bindings.allocate(LHS)->init(Tensor::InitKind::Xavier, 3, mod.getPRNG());
2131 bindings.allocate(RHS)->init(Tensor::InitKind::Xavier, 3, mod.getPRNG());
2132
2133 auto *TNLHS = F->createTanh("tanh", LHS);
2134 auto *MMN = F->createMatMul("matmul", TNLHS, RHS);
2135 auto *TN = F->createTanh("tanh", MMN);
2136 auto *save = F->createSave("ret", TN);
2137 auto *result = save->getPlaceholder();
2138 bindings.allocate(result);
2139
2140 // Note that we are creating profiling info even for nodes that will not be
2141 // quantized. This is how we expect quantizeFunction() to behave, as
2142 // quantization profiling will still get a profile for these nodes.
2143 quantization::QuantizationConfiguration quantConfig{{
2144 {LHS->getOutput().generateNodeOutputName(), {0.3f, 3.0f}},
2145 {TNLHS->getResult().generateNodeOutputName(), {0.4f, 4.0f}},
2146 {RHS->getOutput().generateNodeOutputName(), {0.4f, 4.0f}},
2147 {MMN->getResult().generateNodeOutputName(), {0.6f, 6.0f}},
2148 {TN->getResult().generateNodeOutputName(), {0.5f, 5.0f}},
2149 }};
2150
2151 // Do not quantize any tanh nodes.
2152 KindSet doNotQuantizeKinds;
2153 doNotQuantizeKinds.insert(Kinded::Kind::TanhNodeKind);
2154
2155 quantConfig.assertAllNodesQuantized = true;
2156 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
2157 quantization::quantizeFunction(F, quantConfig, *backend,
2158 /* loweredMap */ {}, doNotQuantizeKinds);
2159
2160 // Make sure that graph can be compiled and run.
2161 ::glow::convertPlaceholdersToConstants(F, bindings, {result});
2162
2163 CompilationContext cctx;
2164 cctx.compMode = CompilationMode::Infer;
2165 // Do not perform any compile-time constant folding.
2166 cctx.optimizationOpts.enableConstantFolding = false;
2167 EE.compile(cctx);
2168
2169 EE.run(bindings);
2170
2171 {
2172 // Verify that the output variable is not quantized, and that it has a
2173 // single save node writer, which is also not quantized.
2174 EXPECT_TRUE(!result->getType()->isQuantizedType());
2175 ASSERT_EQ(result->getUsers().size(), 1);
2176 auto *SN = llvm::dyn_cast<SaveNode>(result->getUsers().begin()->getUser());
2177 ASSERT_TRUE(SN);
2178 EXPECT_TRUE(!SN->getOutput().getType()->isQuantizedType());
2179
2180 // Verify that the tanh is not quantized.
2181 auto *TN1 = llvm::dyn_cast<TanhNode>(SN->getInput());
2182 ASSERT_TRUE(TN1);
2183 EXPECT_TRUE(!TN1->getResult().getType()->isQuantizedType());
2184
2185 // Verify that the input to the tanh is a dequantize node.
2186 auto *DN = llvm::dyn_cast<DequantizeNode>(TN1->getInput());
2187 ASSERT_TRUE(DN);
2188
2189 // Verify that the matmul is quantized.
2190 auto *MMN = llvm::dyn_cast<MatMulNode>(DN->getInput());
2191 ASSERT_TRUE(MMN);
2192 EXPECT_TRUE(MMN->getResult().getType()->isQuantizedType());
2193
2194 // Verify that the LHS input is a quantize node.
2195 auto *QN = llvm::dyn_cast<QuantizeNode>(MMN->getLHS());
2196 ASSERT_TRUE(QN);
2197
2198 // Verify that the second tanh node is also not quantized.
2199 auto *TN2 = llvm::dyn_cast<TanhNode>(QN->getInput());
2200 ASSERT_TRUE(TN2);
2201 EXPECT_TRUE(!TN2->getResult().getType()->isQuantizedType());
2202
2203 // Verify that the input variable to the tanh is not quantized.
2204 auto *varTN2 = llvm::dyn_cast<Constant>(TN2->getInput());
2205 ASSERT_TRUE(varTN2);
2206 EXPECT_TRUE(!varTN2->getType()->isQuantizedType());
2207
2208 // Verify that the RHS input to the matmul is a quantized variable.
2209 auto *RHS = llvm::dyn_cast<Constant>(MMN->getRHS());
2210 ASSERT_TRUE(RHS);
2211 EXPECT_TRUE(RHS->getType()->isQuantizedType());
2212 }
2213 }
2214
2215 /// Test option to disable quantization of multiple specific node kinds in the
2216 /// graph.
TEST(Quantization,quantizeGraphPartiallyMultipleKinds)2217 TEST(Quantization, quantizeGraphPartiallyMultipleKinds) {
2218 ExecutionEngine EE{};
2219 PlaceholderBindings bindings;
2220 auto &mod = EE.getModule();
2221 Function *F = mod.createFunction("main");
2222
2223 auto *LHS = mod.createPlaceholder(ElemKind::FloatTy, {3, 3}, "lhs", true);
2224 auto *RHS = mod.createPlaceholder(ElemKind::FloatTy, {3, 3}, "rhs", true);
2225 bindings.allocate(LHS)->init(Tensor::InitKind::Xavier, 3, mod.getPRNG());
2226 bindings.allocate(RHS)->init(Tensor::InitKind::Xavier, 3, mod.getPRNG());
2227
2228 auto *MMN = F->createMatMul("matmul", LHS, RHS);
2229 auto *CN = F->createAdd("concat", LHS, MMN);
2230 auto *TN = F->createTanh("tanh", CN);
2231 auto *save = F->createSave("ret", TN);
2232 auto *result = save->getPlaceholder();
2233 bindings.allocate(result);
2234
2235 // Note that we are creating profiling info even for nodes that will not be
2236 // quantized. This is how we expect quantizeFunction() to behave, as
2237 // quantization profiling will still get a profile for these nodes.
2238 quantization::QuantizationConfiguration quantConfig{{
2239 {LHS->getOutput().generateNodeOutputName(), {0.3f, 3.0f}},
2240 {RHS->getOutput().generateNodeOutputName(), {0.4f, 4.0f}},
2241 {MMN->getResult().generateNodeOutputName(), {0.6f, 6.0f}},
2242 {CN->getResult().generateNodeOutputName(), {0.6f, 6.0f}},
2243 {TN->getResult().generateNodeOutputName(), {0.5f, 5.0f}},
2244 }};
2245
2246 // Do not quantize any tanh or add nodes.
2247 KindSet doNotQuantizeKinds;
2248 doNotQuantizeKinds.insert(Kinded::Kind::TanhNodeKind);
2249 doNotQuantizeKinds.insert(Kinded::Kind::AddNodeKind);
2250
2251 quantConfig.assertAllNodesQuantized = true;
2252 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
2253 quantization::quantizeFunction(F, quantConfig, *backend,
2254 /* loweredMap */ {}, doNotQuantizeKinds);
2255
2256 // Make sure that graph can be compiled and run.
2257 ::glow::convertPlaceholdersToConstants(F, bindings, {result});
2258
2259 CompilationContext cctx;
2260 cctx.compMode = CompilationMode::Infer;
2261 // Do not perform any compile-time constant folding.
2262 cctx.optimizationOpts.enableConstantFolding = false;
2263 EE.compile(cctx);
2264
2265 EE.run(bindings);
2266
2267 {
2268 // Verify that the output variable is not quantized, and that it has a
2269 // single save node writer, which is also not quantized.
2270 EXPECT_TRUE(!result->getType()->isQuantizedType());
2271 ASSERT_EQ(result->getUsers().size(), 1);
2272 auto *SN = llvm::dyn_cast<SaveNode>(result->getUsers().begin()->getUser());
2273 ASSERT_TRUE(SN);
2274 EXPECT_TRUE(!SN->getOutput().getType()->isQuantizedType());
2275
2276 // Verify that the tanh is not quantized.
2277 auto *TN = llvm::dyn_cast<TanhNode>(SN->getInput());
2278 ASSERT_TRUE(TN);
2279 EXPECT_TRUE(!TN->getResult().getType()->isQuantizedType());
2280
2281 // Verify that the input to the tanh is a non-quantized add node.
2282 auto *AN = llvm::dyn_cast<AddNode>(TN->getInput());
2283 ASSERT_TRUE(AN);
2284 EXPECT_TRUE(!TN->getResult().getType()->isQuantizedType());
2285
2286 // Verify that the LHS input to the AddNode is an unquantized variable.
2287 auto varANLHS = llvm::dyn_cast<Constant>(AN->getLHS());
2288 ASSERT_TRUE(varANLHS);
2289 EXPECT_TRUE(!varANLHS->getType()->isQuantizedType());
2290
2291 // Verify that the RHS input to the AddNode is a dequantize node.
2292 auto *DN = llvm::dyn_cast<DequantizeNode>(AN->getRHS());
2293 ASSERT_TRUE(DN);
2294
2295 // Verify that the matmul is quantized.
2296 auto *MMN = llvm::dyn_cast<MatMulNode>(DN->getInput());
2297 ASSERT_TRUE(MMN);
2298 EXPECT_TRUE(MMN->getResult().getType()->isQuantizedType());
2299
2300 // Verify that the variable inputs to the matmul are quantized.
2301 auto *LHS = llvm::dyn_cast<Constant>(MMN->getLHS());
2302 ASSERT_TRUE(LHS);
2303 EXPECT_TRUE(LHS->getType()->isQuantizedType());
2304
2305 auto *RHS = llvm::dyn_cast<Constant>(MMN->getRHS());
2306 ASSERT_TRUE(RHS);
2307 EXPECT_TRUE(RHS->getType()->isQuantizedType());
2308 }
2309 }
2310
2311 /// Check that quantizeFunction directly converts the constants
2312 /// instead of leaving quantize node around.
TEST(Quantization,quantizeFunctionConvertConstant)2313 TEST(Quantization, quantizeFunctionConvertConstant) {
2314 ExecutionEngine EE{};
2315 PlaceholderBindings bindings;
2316 auto &mod = EE.getModule();
2317 Function *F = mod.createFunction("main");
2318
2319 auto *LHS = mod.createPlaceholder(ElemKind::FloatTy, {3, 3}, "lhs", true);
2320 auto *RHS = mod.createConstant(ElemKind::FloatTy, {3, 3}, "rhs");
2321 bindings.allocate(LHS)->init(Tensor::InitKind::Xavier, 3, mod.getPRNG());
2322 RHS->getPayloadMutable().init(Tensor::InitKind::Xavier, 3, mod.getPRNG());
2323
2324 auto *MMN = F->createMatMul("matmul", LHS, RHS);
2325 auto *save = F->createSave("ret", MMN);
2326 auto *result = save->getPlaceholder();
2327 bindings.allocate(result);
2328
2329 // Note that we are creating profiling info even for nodes that will not be
2330 // quantized. This is how we expect quantizeFunction() to behave, as
2331 // quantization profiling will still get a profile for these nodes.
2332 quantization::QuantizationConfiguration quantConfig{{
2333 {LHS->getOutput().generateNodeOutputName(), {0.3f, 3.0f}},
2334 {RHS->getOutput().generateNodeOutputName(), {0.4f, 4.0f}},
2335 {MMN->getResult().generateNodeOutputName(), {0.6f, 6.0f}},
2336 }};
2337
2338 quantConfig.assertAllNodesQuantized = true;
2339 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
2340 quantization::quantizeFunction(F, quantConfig, *backend);
2341
2342 optimize(F, CompilationMode::Infer);
2343 CompilationContext cctx;
2344 convertQuantizedConstants(F, cctx);
2345
2346 {
2347 // Verify that the output variable is not quantized, and that it has a
2348 // single save node writer, which is also not quantized.
2349 EXPECT_TRUE(!result->getType()->isQuantizedType());
2350 ASSERT_EQ(result->getUsers().size(), 1);
2351 auto *SN = llvm::dyn_cast<SaveNode>(result->getUsers().begin()->getUser());
2352 ASSERT_TRUE(SN);
2353 EXPECT_TRUE(!SN->getOutput().getType()->isQuantizedType());
2354
2355 // Verify that the input to save is a dequantize node.
2356 auto *DN = llvm::dyn_cast<DequantizeNode>(SN->getInput());
2357 ASSERT_TRUE(DN);
2358
2359 // Verify that the matmul is quantized.
2360 auto *MMN = llvm::dyn_cast<MatMulNode>(DN->getInput());
2361 ASSERT_TRUE(MMN);
2362 EXPECT_TRUE(MMN->getResult().getType()->isQuantizedType());
2363
2364 // Verify that the variable inputs to the matmul are quantized.
2365 auto *LHSQuantize = llvm::dyn_cast<QuantizeNode>(MMN->getLHS());
2366 ASSERT_TRUE(LHSQuantize);
2367 EXPECT_EQ(LHSQuantize->getInput().getNode(), LHS);
2368
2369 auto *RHS = llvm::dyn_cast<Constant>(MMN->getRHS());
2370 ASSERT_TRUE(RHS);
2371 EXPECT_TRUE(RHS->getType()->isQuantizedType());
2372 }
2373
2374 // Make sure that graph can be compiled and run.
2375 EE.compile(CompilationMode::Infer);
2376
2377 EE.run(bindings);
2378 }
2379
2380 /// Check that the slice node doesn't change the quantization parameters between
2381 /// its input and output.
TEST(Quantization,quantizeSlice)2382 TEST(Quantization, quantizeSlice) {
2383 ExecutionEngine EE{};
2384 PlaceholderBindings bindings;
2385 auto &mod = EE.getModule();
2386 Function *F = mod.createFunction("main");
2387
2388 auto *input = mod.createPlaceholder(ElemKind::FloatTy, {4}, "input", true);
2389 bindings.allocate(input)->init(Tensor::InitKind::Xavier, 3, mod.getPRNG());
2390
2391 auto *slice = F->createSlice("slice", input, {2}, {3});
2392 auto *save = F->createSave("ret", slice);
2393 auto *result = save->getPlaceholder();
2394 bindings.allocate(result);
2395
2396 quantization::QuantizationConfiguration quantConfig{{
2397 {slice->getResult().generateNodeOutputName(), {0.2f, 2.0f}},
2398 {input->getOutput().generateNodeOutputName(), {0.4f, 4.0f}},
2399 }};
2400
2401 // Compute quantization parameters for verification.
2402 auto sliceInpTQP = chooseQuantizationParams({0.4, 4.0}, quantConfig.schema,
2403 quantConfig.precision);
2404
2405 quantConfig.assertAllNodesQuantized = true;
2406 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
2407 quantization::quantizeFunction(F, quantConfig, *backend);
2408
2409 optimize(F, CompilationMode::Infer);
2410
2411 {
2412 // Verify that the output variable is not quantized, and that it has a
2413 // single save node writer, which is also not quantized.
2414 EXPECT_TRUE(!result->getType()->isQuantizedType());
2415 ASSERT_EQ(result->getUsers().size(), 1);
2416 auto *SN = llvm::dyn_cast<SaveNode>(result->getUsers().begin()->getUser());
2417 ASSERT_TRUE(SN);
2418 EXPECT_TRUE(!SN->getOutput().getType()->isQuantizedType());
2419
2420 // Verify that the input to save is a dequantize node.
2421 auto *DN = llvm::dyn_cast<DequantizeNode>(SN->getInput());
2422 ASSERT_TRUE(DN);
2423
2424 // Verify that the slice is rescaled after being quantized.
2425 // The reason we need a rescale is because slicing doesn't perform rescaling
2426 // by itself.
2427 // Note: after optimization, the RescaleQuantized node created for the Slice
2428 // gets merged with the dequantize node.
2429 auto *qslice = llvm::dyn_cast<SliceNode>(DN->getInput());
2430 ASSERT_TRUE(qslice);
2431 ASSERT_TRUE(qslice->getResult().getType()->isQuantizedType());
2432 EXPECT_EQ(qslice->getResult().getType()->getOffset(), sliceInpTQP.offset);
2433 EXPECT_EQ(qslice->getResult().getType()->getScale(), sliceInpTQP.scale);
2434
2435 // Verify that the variable inputs to the matmul are quantized.
2436 auto *qinput = llvm::dyn_cast<QuantizeNode>(qslice->getInput());
2437 ASSERT_TRUE(qinput);
2438 EXPECT_EQ(qinput->getResult().getType()->getOffset(),
2439 qslice->getResult().getType()->getOffset());
2440 EXPECT_EQ(qinput->getResult().getType()->getScale(),
2441 qslice->getResult().getType()->getScale());
2442 EXPECT_EQ(qinput->getInput().getNode(), input);
2443 }
2444
2445 // Make sure that graph can be compiled and run.
2446 EE.compile(CompilationMode::Infer);
2447
2448 EE.run(bindings);
2449 }
2450
2451 /// Check that the reshape node doesn't change the quantization parameters
2452 /// between its input and output.
TEST(Quantization,quantizeReshape)2453 TEST(Quantization, quantizeReshape) {
2454 ExecutionEngine EE{};
2455 PlaceholderBindings bindings;
2456 auto &mod = EE.getModule();
2457 Function *F = mod.createFunction("main");
2458
2459 auto *input = mod.createPlaceholder(ElemKind::FloatTy, {3, 3}, "input", true);
2460 bindings.allocate(input)->init(Tensor::InitKind::Xavier, 3, mod.getPRNG());
2461
2462 auto *reshape = F->createReshape("reshape", input, {9});
2463 auto *save = F->createSave("ret", reshape);
2464 auto *result = save->getPlaceholder();
2465 bindings.allocate(result);
2466
2467 quantization::QuantizationConfiguration quantConfig{{
2468 {reshape->getResult().generateNodeOutputName(), {0.2f, 2.0f}},
2469 {input->getOutput().generateNodeOutputName(), {0.4f, 4.0f}},
2470 }};
2471
2472 // Compute quantization parameters for verification.
2473 auto reshapeInpTQP = chooseQuantizationParams({0.4, 4.0}, quantConfig.schema,
2474 quantConfig.precision);
2475
2476 quantConfig.assertAllNodesQuantized = true;
2477 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
2478 quantization::quantizeFunction(F, quantConfig, *backend);
2479
2480 optimize(F, CompilationMode::Infer);
2481
2482 {
2483 // Verify that the output variable is not quantized, and that it has a
2484 // single save node writer, which is also not quantized.
2485 EXPECT_TRUE(!result->getType()->isQuantizedType());
2486 ASSERT_EQ(result->getUsers().size(), 1);
2487 auto *SN = llvm::dyn_cast<SaveNode>(result->getUsers().begin()->getUser());
2488 ASSERT_TRUE(SN);
2489 EXPECT_TRUE(!SN->getOutput().getType()->isQuantizedType());
2490
2491 // Verify that the input to save is a dequantize node.
2492 auto *DN = llvm::dyn_cast<DequantizeNode>(SN->getInput());
2493 ASSERT_TRUE(DN);
2494
2495 // Verify that the reshape is rescaled after being quantized.
2496 // The reason we need a rescale is because reshaping doesn't perform
2497 // rescaling by itself.
2498 // Note: after optimization, the RescaleQuantized node created for the
2499 // Reshape gets merged with the dequantize node.
2500 auto *qreshape = llvm::dyn_cast<ReshapeNode>(DN->getInput());
2501 ASSERT_TRUE(qreshape);
2502 ASSERT_TRUE(qreshape->getResult().getType()->isQuantizedType());
2503 EXPECT_EQ(qreshape->getResult().getType()->getOffset(),
2504 reshapeInpTQP.offset);
2505 EXPECT_EQ(qreshape->getResult().getType()->getScale(), reshapeInpTQP.scale);
2506
2507 // Verify that the variable inputs to the matmul are quantized.
2508 auto *qinput = llvm::dyn_cast<QuantizeNode>(qreshape->getInput());
2509 ASSERT_TRUE(qinput);
2510 EXPECT_EQ(qinput->getResult().getType()->getOffset(),
2511 qreshape->getResult().getType()->getOffset());
2512 EXPECT_EQ(qinput->getResult().getType()->getScale(),
2513 qreshape->getResult().getType()->getScale());
2514 EXPECT_EQ(qinput->getInput().getNode(), input);
2515 }
2516
2517 // Make sure that graph can be compiled and run.
2518 EE.compile(CompilationMode::Infer);
2519
2520 EE.run(bindings);
2521 }
2522
2523 /// Mock backend that does not lower FC nodes.
2524 class MockBackendUnloweredFC : public MockBackend {
shouldLower(const Node * N) const2525 bool shouldLower(const Node *N) const override {
2526 if (N->getKind() == Kinded::Kind::FullyConnectedNodeKind) {
2527 return false;
2528 }
2529 return true;
2530 }
isOpSupported(const NodeInfo & NI) const2531 bool isOpSupported(const NodeInfo &NI) const override { return true; }
2532 };
2533
2534 /// Mock backend that does lower FC nodes.
2535 class MockBackendLoweredFC : public MockBackend {
shouldLower(const Node * N) const2536 bool shouldLower(const Node *N) const override { return true; }
isOpSupported(const NodeInfo & NI) const2537 bool isOpSupported(const NodeInfo &NI) const override { return true; }
2538 };
2539
2540 /// Create a simple network with an FC given \p bindings, \p EE, and \p F.
2541 /// \returns the FC node.
createSimpleFCNet(PlaceholderBindings & bindings,ExecutionEngine & EE,Function & F)2542 static FullyConnectedNode *createSimpleFCNet(PlaceholderBindings &bindings,
2543 ExecutionEngine &EE, Function &F) {
2544 auto &mod = EE.getModule();
2545 auto *input = mod.createPlaceholder(ElemKind::FloatTy, {1, 3}, "input", true);
2546 auto *W = mod.createPlaceholder(ElemKind::FloatTy, {3, 3}, "weights", true);
2547 auto *B = mod.createPlaceholder(ElemKind::FloatTy, {3}, "bias", true);
2548
2549 bindings.allocate(input);
2550 bindings.allocate(W)->init(Tensor::InitKind::Xavier, 3, mod.getPRNG());
2551 bindings.allocate(B)->init(Tensor::InitKind::Broadcast, 0.1, mod.getPRNG());
2552
2553 auto *FC = F.createFullyConnected("FC", input, W, B);
2554 auto *S = F.createSave("ret", FC);
2555 ::glow::convertPlaceholdersToConstants(&F, bindings,
2556 {input, S->getPlaceholder()});
2557 bindings.allocate(S->getPlaceholder());
2558
2559 return FC;
2560 }
2561
2562 /// Helper to look for a node with kind \p NodeClass in \p F. If found, \returns
2563 /// a pointer to the node. Otherwise \returns a nullptr.
2564 template <class NodeClass>
findNodeKindOrReturnNull(Function * F)2565 static NodeClass *findNodeKindOrReturnNull(Function *F) {
2566 auto it = std::find_if(
2567 F->getNodes().begin(), F->getNodes().end(),
2568 [](const Node &node) -> bool { return llvm::isa<NodeClass>(&node); });
2569 if (it == F->getNodes().end()) {
2570 return nullptr;
2571 }
2572 return &llvm::cast<NodeClass>(*it);
2573 }
2574
2575 /// Profile and quantize a graph with an FC, and make sure that we find the
2576 /// correct quantization parameters, whether the \p BackendClass does or does
2577 /// not lower the FC given \p expectLoweredFC. Note that in this test we
2578 /// replicate the logic from optimizeFunction(), wherein we lower and then call
2579 /// profileQuantization(), in order to ensure each stage of the compilation
2580 /// pipeline for profiling/quantization is correct.
2581 template <class BackendClass>
testProfileQuantizationOfFC(bool expectLoweredFC,bool rowwiseQuantizeFC)2582 static void testProfileQuantizationOfFC(bool expectLoweredFC,
2583 bool rowwiseQuantizeFC) {
2584 ExecutionEngine profileEE{};
2585 Function *profileF = profileEE.getModule().createFunction("profile");
2586 PlaceholderBindings profilebindings;
2587 FullyConnectedNode *FC =
2588 createSimpleFCNet(profilebindings, profileEE, *profileF);
2589 auto outputNameFC = FC->getResult().generateNodeOutputName();
2590 auto weightsNameFC = FC->getWeights().generateNodeOutputName();
2591 auto biasNameFC = FC->getBias().generateNodeOutputName();
2592 auto inputNameFC = FC->getInput().generateNodeOutputName();
2593
2594 // Lower everything and keep track of the lowered components source nodes via
2595 // the loweredMap.
2596 LoweredInfoMap loweredMapForProf;
2597 CompilationContext cctx(/* bindings */ nullptr, &loweredMapForProf);
2598 lower(profileF, cctx);
2599
2600 // Check that the lowered graph only contains the lowered components of the
2601 // FC (MM and BA) and not the FC itself.
2602 auto *loweredFC = findNodeKindOrReturnNull<FullyConnectedNode>(profileF);
2603 auto *loweredMM = findNodeKindOrReturnNull<MatMulNode>(profileF);
2604 auto *loweredBA = findNodeKindOrReturnNull<BatchedAddNode>(profileF);
2605 ASSERT_FALSE(loweredFC);
2606 ASSERT_TRUE(loweredMM);
2607 ASSERT_TRUE(loweredBA);
2608 auto outputNameMM = loweredMM->getResult().generateNodeOutputName();
2609 auto outputNameBA = loweredBA->getResult().generateNodeOutputName();
2610
2611 glow::profileQuantization(profilebindings, profileF,
2612 cctx.precisionConfig.profConfig);
2613
2614 // Compile/run to capture profile.
2615 profileEE.compile(CompilationMode::Infer);
2616 profileEE.run(profilebindings);
2617
2618 // Get profiling infos and build new quantized graph, passing in the
2619 // loweredMapForProf to include the unlowered components in QI.
2620 profileF = profileEE.getModule().getFunctions().front();
2621 quantization::QuantizationConfiguration quantConfig{
2622 quantization::generateNodeProfilingInfos(profilebindings, profileF,
2623 loweredMapForProf)};
2624
2625 // Verify that we have node profiling infos for the FC and the lowered
2626 // components of the FC (MM and BA).
2627 NodeProfilingInfo *FCPI = nullptr, *MMPI = nullptr, *BAPI = nullptr,
2628 *FCWPI = nullptr, *FCBPI = nullptr, *FCIPI = nullptr;
2629 for (NodeProfilingInfo &NPI : quantConfig.infos) {
2630 if (NPI.nodeOutputName_ == outputNameFC) {
2631 FCPI = &NPI;
2632 } else if (NPI.nodeOutputName_ == outputNameMM) {
2633 MMPI = &NPI;
2634 } else if (NPI.nodeOutputName_ == outputNameBA) {
2635 BAPI = &NPI;
2636 } else if (NPI.nodeOutputName_ == weightsNameFC) {
2637 FCWPI = &NPI;
2638 } else if (NPI.nodeOutputName_ == biasNameFC) {
2639 FCBPI = &NPI;
2640 } else if (NPI.nodeOutputName_ == inputNameFC) {
2641 FCIPI = &NPI;
2642 }
2643 }
2644 ASSERT_TRUE(FCPI);
2645 ASSERT_TRUE(MMPI);
2646 ASSERT_TRUE(BAPI);
2647 ASSERT_TRUE(FCWPI);
2648 ASSERT_TRUE(FCBPI);
2649 ASSERT_TRUE(FCIPI);
2650
2651 // Compute quantization parameters for verification.
2652 auto FCTQP = chooseQuantizationParams(
2653 FCPI->tensorProfilingParams_, quantConfig.schema, quantConfig.precision);
2654 auto MMTQP = chooseQuantizationParams(
2655 MMPI->tensorProfilingParams_, quantConfig.schema, quantConfig.precision);
2656 auto BATQP = chooseQuantizationParams(
2657 BAPI->tensorProfilingParams_, quantConfig.schema, quantConfig.precision);
2658 auto FCWTQP = chooseQuantizationParams(
2659 FCWPI->tensorProfilingParams_, quantConfig.schema, quantConfig.precision);
2660 auto FCBTQP =
2661 chooseQuantizationParams(FCBPI->tensorProfilingParams_,
2662 quantConfig.schema, quantConfig.precisionBias);
2663 auto FCITQP = chooseQuantizationParams(
2664 FCIPI->tensorProfilingParams_, quantConfig.schema, quantConfig.precision);
2665
2666 // Now create the same original function in the backend we're testing.
2667 ExecutionEngine backendEE;
2668 BackendClass backend;
2669 Backend *backendPtr = &backend;
2670 // backendEE.setBackend(&backend, /* ownsBackend */ false);
2671 Function *backendF = backendEE.getModule().createFunction("quantized");
2672 PlaceholderBindings backendbindings;
2673 createSimpleFCNet(backendbindings, backendEE, *backendF);
2674
2675 // Lower the function given the backend's preferences for lowering.
2676 LoweredInfoMap loweredMapForQuant;
2677 CompilationContext cctx2(/* bindings */ nullptr, &loweredMapForQuant);
2678 lower(backendF, cctx2, backendPtr);
2679
2680 // Check that the backend lowered the function as expected.
2681 auto *floatFC = findNodeKindOrReturnNull<FullyConnectedNode>(backendF);
2682 auto *floatMM = findNodeKindOrReturnNull<MatMulNode>(backendF);
2683 auto *floatBA = findNodeKindOrReturnNull<BatchedAddNode>(backendF);
2684 if (expectLoweredFC) {
2685 ASSERT_FALSE(floatFC);
2686 ASSERT_TRUE(floatMM);
2687 ASSERT_TRUE(floatBA);
2688 } else {
2689 ASSERT_TRUE(floatFC);
2690 ASSERT_FALSE(floatMM);
2691 ASSERT_FALSE(floatBA);
2692 }
2693
2694 // Quantize the function given the current backend we're testing along with
2695 // the quantization infos gathered.
2696 quantConfig.enableRowwise = rowwiseQuantizeFC;
2697 quantConfig.assertAllNodesQuantized = true;
2698 quantization::quantizeFunction(backendF, quantConfig, *backendPtr,
2699 loweredMapForQuant);
2700
2701 // Optimize the graph to remove dead code and optimize away unnecessary
2702 // quantize nodes. Note that we do not do a full compile call here, as we have
2703 // already lowered.
2704 ::glow::optimize(backendF, CompilationMode::Infer);
2705
2706 // Check that the graph is still structured as expected, and that the
2707 // scales/offsets are set as found in TQP.
2708 auto *quantFC = findNodeKindOrReturnNull<FullyConnectedNode>(backendF);
2709 auto *quantMM = findNodeKindOrReturnNull<MatMulNode>(backendF);
2710 auto *quantBA = findNodeKindOrReturnNull<BatchedAddNode>(backendF);
2711 auto *quantRowwiseFC =
2712 findNodeKindOrReturnNull<RowwiseQuantizedFullyConnectedNode>(backendF);
2713
2714 if (rowwiseQuantizeFC) {
2715 EXPECT_FALSE(quantMM);
2716 EXPECT_FALSE(quantBA);
2717 EXPECT_FALSE(quantFC);
2718
2719 ASSERT_TRUE(quantRowwiseFC);
2720 EXPECT_EQ(quantRowwiseFC->getResult().getType()->getScale(), FCTQP.scale);
2721 EXPECT_EQ(quantRowwiseFC->getResult().getType()->getOffset(), FCTQP.offset);
2722
2723 EXPECT_EQ(quantRowwiseFC->getBias().getElementType(), ElemKind::Int32QTy);
2724 // Bias scale was changed with the product inputScale * weightsScale only
2725 // if the product was larger.
2726 if (FCWTQP.scale * FCITQP.scale > FCBTQP.scale) {
2727 EXPECT_EQ(quantRowwiseFC->getBias().getType()->getScale(),
2728 FCWTQP.scale * FCITQP.scale);
2729 EXPECT_EQ(quantRowwiseFC->getBias().getType()->getOffset(), 0);
2730 } else {
2731 EXPECT_EQ(quantRowwiseFC->getBias().getType()->getScale(), FCBTQP.scale);
2732 EXPECT_EQ(quantRowwiseFC->getBias().getType()->getOffset(), 0);
2733 }
2734 } else if (expectLoweredFC) {
2735 ASSERT_FALSE(quantFC);
2736 ASSERT_FALSE(quantRowwiseFC);
2737
2738 ASSERT_TRUE(quantMM);
2739 EXPECT_EQ(quantMM->getResult().getType()->getScale(), MMTQP.scale);
2740 EXPECT_EQ(quantMM->getResult().getType()->getOffset(), MMTQP.offset);
2741
2742 ASSERT_TRUE(quantBA);
2743 EXPECT_EQ(quantBA->getResult().getType()->getScale(), BATQP.scale);
2744 EXPECT_EQ(quantBA->getResult().getType()->getOffset(), BATQP.offset);
2745
2746 EXPECT_EQ(quantBA->getSlice().getElementType(), ElemKind::Int32QTy);
2747 // Bias scale was changed with the product inputScale * weightsScale only
2748 // if the product was larger.
2749 if (FCWTQP.scale * FCITQP.scale > FCBTQP.scale) {
2750 EXPECT_EQ(quantBA->getSlice().getType()->getScale(),
2751 FCWTQP.scale * FCITQP.scale);
2752 EXPECT_EQ(quantBA->getSlice().getType()->getOffset(), 0);
2753 } else {
2754 EXPECT_EQ(quantBA->getSlice().getType()->getScale(), FCBTQP.scale);
2755 EXPECT_EQ(quantBA->getSlice().getType()->getOffset(), 0);
2756 }
2757 } else {
2758 ASSERT_FALSE(quantRowwiseFC);
2759
2760 ASSERT_TRUE(quantFC);
2761 EXPECT_EQ(quantFC->getResult().getType()->getScale(), FCTQP.scale);
2762 EXPECT_EQ(quantFC->getResult().getType()->getOffset(), FCTQP.offset);
2763
2764 ASSERT_FALSE(quantMM);
2765 ASSERT_FALSE(quantBA);
2766
2767 EXPECT_EQ(quantFC->getBias().getElementType(), ElemKind::Int32QTy);
2768 // Bias scale was changed with the product inputScale * weightsScale only
2769 // if the product was larger.
2770 if (FCWTQP.scale * FCITQP.scale > FCBTQP.scale) {
2771 EXPECT_EQ(quantFC->getBias().getType()->getScale(),
2772 FCWTQP.scale * FCITQP.scale);
2773 EXPECT_EQ(quantFC->getBias().getType()->getOffset(), 0);
2774 } else {
2775 EXPECT_EQ(quantFC->getBias().getType()->getScale(), FCBTQP.scale);
2776 EXPECT_EQ(quantFC->getBias().getType()->getOffset(), 0);
2777 }
2778 }
2779 }
2780
2781 /// Test that backends that do not lower FCs can find the quantization
2782 /// parameters of their nodes.
TEST(Quantization,TestProfileQuantizationOfUnloweredFC)2783 TEST(Quantization, TestProfileQuantizationOfUnloweredFC) {
2784 testProfileQuantizationOfFC<MockBackendUnloweredFC>(
2785 /* expectLoweredFC */ false, /* rowwiseQuantizeFC */ false);
2786 }
2787
2788 /// Test that backends that do lower FCs can find the quantization parameters of
2789 /// their nodes.
TEST(Quantization,TestProfileQuantizationOfLoweredFC)2790 TEST(Quantization, TestProfileQuantizationOfLoweredFC) {
2791 testProfileQuantizationOfFC<MockBackendLoweredFC>(
2792 /* expectLoweredFC */ true, /* rowwiseQuantizeFC */ false);
2793 }
2794
2795 /// Test that backends that do not lower FCs can find the quantization
2796 /// parameters of their nodes and correctly rowwise quantize.
TEST(Quantization,TestProfileQuantizationOfUnloweredFCRowwise)2797 TEST(Quantization, TestProfileQuantizationOfUnloweredFCRowwise) {
2798 testProfileQuantizationOfFC<MockBackendUnloweredFC>(
2799 /* expectLoweredFC */ false, /* rowwiseQuantizeFC */ true);
2800 }
2801
2802 /// Test that backends that do lower FCs can find the quantization parameters of
2803 /// their nodes and correctly rowwise quantize even when lowering the FC.
TEST(Quantization,TestProfileQuantizationOfLoweredFCRowwise)2804 TEST(Quantization, TestProfileQuantizationOfLoweredFCRowwise) {
2805 testProfileQuantizationOfFC<MockBackendLoweredFC>(
2806 /* expectLoweredFC */ true, /* rowwiseQuantizeFC */ true);
2807 }
2808
2809 /// Check that asserting quantization for the quantizer works as expected.
TEST(Quantization,CheckAssertQuantization)2810 TEST(Quantization, CheckAssertQuantization) {
2811 ExecutionEngine EE{};
2812 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
2813 auto &mod = EE.getModule();
2814 Function *F = mod.createFunction("main");
2815 auto *input = mod.createPlaceholder(ElemKind::FloatTy, {1, 3}, "input", true);
2816 auto *relu = F->createRELU("ReLU", input);
2817 PlaceholderBindings bindings;
2818 auto *save = F->createSave("ret", relu);
2819 bindings.allocate(save->getPlaceholder());
2820
2821 quantization::QuantizationConfiguration quantConfig{
2822 {{input->getOutput().generateNodeOutputName(), {0.2f, 2.0f}},
2823 {relu->getResult().generateNodeOutputName(), {0.2f, 3.0f}}}};
2824 quantConfig.precision = ElemKind::Int16QTy;
2825 quantConfig.assertAllNodesQuantized = true;
2826
2827 // Expect this to die because quantizeFunction() is passed with
2828 // assertAllNodesQuantized true, and the Interpreter backend does not support
2829 // Int16QTy ReLU.
2830 Function *QF = F->clone("quant_clone1");
2831 EXPECT_DEATH(quantization::quantizeFunction(QF, quantConfig, *backend), "");
2832
2833 {
2834 Function *QF = F->clone("quant_clone2");
2835 quantConfig.assertAllNodesQuantized = false;
2836
2837 // This works fine because quantizeFunction() is passed with
2838 // assertAllNodesQuantized false, and so the ReLU will not be quantized as
2839 // the Interpreter does not support Int16QTy ReLU.
2840 quantization::quantizeFunction(QF, quantConfig, *backend);
2841
2842 auto *saveNode =
2843 llvm::dyn_cast<SaveNode>(QF->getNodeByName(save->getName()));
2844 ASSERT_TRUE(saveNode);
2845 auto *reluNode = llvm::dyn_cast<ReluNode>(saveNode->getInput().getNode());
2846 ASSERT_TRUE(reluNode);
2847 EXPECT_TRUE(!reluNode->getResult().getType()->isQuantizedType());
2848 }
2849
2850 {
2851 Function *QF = F->clone("quant_clone3");
2852 quantConfig.assertAllNodesQuantized = true;
2853 KindSet doNotQuantizeKinds;
2854 doNotQuantizeKinds.insert(Kinded::Kind::ReluNodeKind);
2855
2856 // This works fine because quantizeFunction() is passed with
2857 // assertAllNodesQuantized true, but we explicitly tell the quantizer to
2858 // keep ReLU in its original precision.
2859 quantization::quantizeFunction(QF, quantConfig, *backend,
2860 /* loweredMap */ {}, doNotQuantizeKinds);
2861
2862 auto *saveNode =
2863 llvm::dyn_cast<SaveNode>(QF->getNodeByName(save->getName()));
2864 ASSERT_TRUE(saveNode);
2865 auto *reluNode = llvm::dyn_cast<ReluNode>(saveNode->getInput().getNode());
2866 ASSERT_TRUE(reluNode);
2867 EXPECT_TRUE(!reluNode->getResult().getType()->isQuantizedType());
2868 }
2869 }
2870
2871 /// Check that we can quantize nodes that have some quantized outputs as unused,
2872 /// e.g. a TopK node where values is unused but indices is.
TEST(Quantization,QuantizationZeroUsersResult)2873 TEST(Quantization, QuantizationZeroUsersResult) {
2874 ExecutionEngine EE{};
2875 auto &mod = EE.getModule();
2876 PlaceholderBindings bindings;
2877 Function *F = mod.createFunction("main");
2878 auto *input =
2879 mod.createPlaceholder(ElemKind::FloatTy, {3, 1, 5}, "input", false);
2880
2881 bindings.allocate(input)->getHandle() = {
2882 28, 4, 411, 19, 42, 0.4f, 0.4f, 0.4f, -0.4f, 0.45f, 7, 5, 9, 8, 100,
2883 };
2884
2885 // Note we intentionally do not save the topk's values result.
2886 auto *TK = F->createTopK("TopK", input, 3);
2887 auto *SN = F->createSave("save_indices", TK->getIndices());
2888 bindings.allocate(SN->getPlaceholder());
2889
2890 quantization::QuantizationConfiguration quantConfig{
2891 {{input->getOutput().generateNodeOutputName(), {0.2f, 2.0f}},
2892 {TK->getValues().generateNodeOutputName(), {0.2f, 3.0f}}}};
2893 quantConfig.assertAllNodesQuantized = true;
2894
2895 std::unique_ptr<Backend> backend(createBackend(EE.getBackendName()));
2896 quantization::quantizeFunction(F, quantConfig, *backend);
2897
2898 auto *qSN = llvm::dyn_cast<SaveNode>(F->getNodeByName(SN->getName()));
2899 ASSERT_TRUE(qSN);
2900 auto *qTK = llvm::dyn_cast<TopKNode>(qSN->getInput().getNode());
2901 ASSERT_TRUE(qTK);
2902 EXPECT_TRUE(qTK->getValues().getType()->isQuantizedType());
2903 }
2904
2905 GLOW_INSTANTIATE_TEST_SUITE_P(Interpreter, Quantization,
2906 ::testing::Values("Interpreter"));
2907
2908 #ifdef GLOW_WITH_CPU
2909 GLOW_INSTANTIATE_TEST_SUITE_P(CPU, Quantization, ::testing::Values("CPU"));
2910
2911 GLOW_INSTANTIATE_TEST_SUITE_P(
2912 InterpAndCPUProfAndQuant, Operator,
2913 ::testing::Combine(::testing::Values("Interpreter", "CPU"),
2914 ::testing::Values("Interpreter", "CPU")));
2915
2916 GLOW_INSTANTIATE_TEST_SUITE_P(
2917 InterpAndCPUProfAndQuant, InterpAndCPU,
2918 ::testing::Combine(::testing::Values("Interpreter", "CPU"),
2919 ::testing::Values("Interpreter", "CPU")));
2920
2921 #else
2922 GLOW_INSTANTIATE_TEST_SUITE_P(
2923 InterpreterProfAndQuant, Operator,
2924 ::testing::Combine(::testing::Values("Interpreter"),
2925 ::testing::Values("Interpreter")));
2926
2927 GLOW_INSTANTIATE_TEST_SUITE_P(
2928 Interpreter, InterpAndCPU,
2929 ::testing::Combine(::testing::Values("Interpreter"),
2930 ::testing::Values("Interpreter")));
2931 #endif // GLOW_WITH_CPU
2932
2933 #ifdef GLOW_WITH_OPENCL
2934 GLOW_INSTANTIATE_TEST_SUITE_P(
2935 InterpProfOpenCLQuant, Operator,
2936 ::testing::Combine(::testing::Values("Interpreter"),
2937 ::testing::Values("OpenCL")));
2938 #endif // GLOW_WITH_OPENCL
2939
2940 } // namespace glow
2941