Caffe2 - Python API
A deep learning, cross platform ML framework
hypothesis_test_util.py
1 ## @package hypothesis_test_util
2 # Module caffe2.python.hypothesis_test_util
3 """
4 The Hypothesis library uses *property-based testing* to check
5 invariants about the code under test under a variety of random inputs.
6 
7  The key idea here is to express properties of the code under test
8 (e.g. that it passes a gradient check, that it implements a reference
9 function, etc), and then generate random instances and verify they
10 satisfy these properties.
11 
12 The main functions of interest are exposed on `HypothesisTestCase`.
13 You can usually just add a short function in this to generate an
14 arbitrary number of test cases for your operator.
15 
16 The key functions are:
17 
18 - `assertDeviceChecks(devices, op, inputs, outputs)`. This asserts that the
19  operator computes the same outputs, regardless of which device it is executed
20  on.
21 - `assertGradientChecks(device, op, inputs, output_,
22  outputs_with_grads)`. This implements a standard numerical gradient checker
23  for the operator in question.
24 - `assertReferenceChecks(device, op, inputs, reference)`. This runs the
25  reference function (effectively calling `reference(*inputs)`, and comparing
26  that to the output of output.
27 
28 `hypothesis_test_util.py` exposes some useful pre-built samplers.
29 
30 - `hu.gcs` - a gradient checker device (`gc`) and device checker devices (`dc`)
31 
32 - `hu.gcs_cpu_only` - a CPU-only gradient checker device (`gc`) and
33  device checker devices (`dc`). Used for when your operator is only
34  implemented on the CPU.
35 """
36 
37 from __future__ import absolute_import
38 from __future__ import division
39 from __future__ import print_function
40 from __future__ import unicode_literals
41 from caffe2.proto import caffe2_pb2
42 from caffe2.python import (
43  workspace, device_checker, gradient_checker, test_util, core)
44 import contextlib
45 import copy
46 import functools
47 import hypothesis
48 import hypothesis.extra.numpy
49 import hypothesis.strategies as st
50 import logging
51 import numpy as np
52 import os
53 
54 
55 def is_sandcastle():
56  if os.getenv('SANDCASTLE') == '1':
57  return True
58  elif os.getenv('TW_JOB_USER') == 'sandcastle':
59  return True
60  return False
61 
62 
63 def is_travis():
64  return 'TRAVIS' in os.environ
65 
66 
67 hypothesis.settings.register_profile(
68  "sandcastle",
69  hypothesis.settings(
70  derandomize=True,
71  suppress_health_check=[hypothesis.HealthCheck.too_slow],
72  database=None,
73  min_satisfying_examples=1,
74  max_examples=100,
75  verbosity=hypothesis.Verbosity.verbose))
76 
77 hypothesis.settings.register_profile(
78  "dev",
79  hypothesis.settings(
80  suppress_health_check=[hypothesis.HealthCheck.too_slow],
81  database=None,
82  max_examples=10,
83  min_satisfying_examples=1,
84  verbosity=hypothesis.Verbosity.verbose))
85 hypothesis.settings.register_profile(
86  "debug",
87  hypothesis.settings(
88  suppress_health_check=[hypothesis.HealthCheck.too_slow],
89  database=None,
90  max_examples=1000,
91  min_satisfying_examples=1,
92  verbosity=hypothesis.Verbosity.verbose))
93 hypothesis.settings.load_profile(
94  'sandcastle' if is_sandcastle() else os.getenv('CAFFE2_HYPOTHESIS_PROFILE',
95  'dev')
96 )
97 
98 
99 def dims(min_value=1, max_value=5):
100  return st.integers(min_value=min_value, max_value=max_value)
101 
102 
103 def elements_of_type(dtype=np.float32, filter_=None):
104  elems = None
105  if dtype in (np.float16, np.float32, np.float64):
106  elems = st.floats(min_value=-1.0, max_value=1.0)
107  elif dtype is np.int32:
108  elems = st.integers(min_value=0, max_value=2 ** 31 - 1)
109  elif dtype is np.int64:
110  elems = st.integers(min_value=0, max_value=2 ** 63 - 1)
111  elif dtype is np.bool:
112  elems = st.booleans()
113  else:
114  raise ValueError("Unexpected dtype without elements provided")
115  return elems if filter_ is None else elems.filter(filter_)
116 
117 
118 def arrays(dims, dtype=np.float32, elements=None):
119  if elements is None:
120  elements = elements_of_type(dtype)
121  return hypothesis.extra.numpy.arrays(
122  dtype,
123  dims,
124  elements=elements,
125  )
126 
127 
128 def tensor(min_dim=1,
129  max_dim=4,
130  dtype=np.float32,
131  elements=None,
132  **kwargs):
133  dims_ = st.lists(dims(**kwargs), min_size=min_dim, max_size=max_dim)
134  return dims_.flatmap(
135  lambda dims: arrays(dims, dtype, elements))
136 
137 
138 def tensor1d(min_len=1, max_len=64, dtype=np.float32, elements=None):
139  return tensor(1, 1, dtype, elements, min_value=min_len, max_value=max_len)
140 
141 
142 def segment_ids(size, is_sorted):
143  if size == 0:
144  return st.just(np.empty(shape=[0], dtype=np.int32))
145  if is_sorted:
146  return arrays(
147  [size],
148  dtype=np.int32,
149  elements=st.booleans()).map(
150  lambda x: np.cumsum(x, dtype=np.int32) - x[0])
151  else:
152  return arrays(
153  [size],
154  dtype=np.int32,
155  elements=st.integers(min_value=0, max_value=2 * size))
156 
157 
158 def lengths(size, min_segments=None, max_segments=None, **kwargs):
159  # First generate number of boarders between segments
160  # Then create boarder values and add 0 and size
161  # By sorting and computing diff we convert them to lengths of
162  # possible 0 value
163  if min_segments is None:
164  min_segments = 0
165  if max_segments is None:
166  max_segments = size
167  assert min_segments >= 0
168  assert min_segments <= max_segments
169  if size == 0 and max_segments == 0:
170  return st.just(np.empty(shape=[0], dtype=np.int32))
171  assert max_segments > 0, "size is not 0, need at least one segment"
172  return st.integers(
173  min_value=max(min_segments - 1, 0), max_value=max_segments - 1
174  ).flatmap(
175  lambda num_borders:
176  hypothesis.extra.numpy.arrays(
177  np.int32, num_borders, elements=st.integers(
178  min_value=0, max_value=size
179  )
180  )
181  ).map(
182  lambda x: np.append(x, np.array([0, size], dtype=np.int32))
183  ).map(sorted).map(np.diff)
184 
185 
186 def segmented_tensor(
187  min_dim=1,
188  max_dim=4,
189  dtype=np.float32,
190  is_sorted=True,
191  elements=None,
192  segment_generator=segment_ids,
193  allow_empty=False,
194  **kwargs
195 ):
196  gen_empty = st.booleans() if allow_empty else st.just(False)
197  data_dims_ = st.lists(dims(**kwargs), min_size=min_dim, max_size=max_dim)
198  data_dims_ = st.tuples(
199  gen_empty, data_dims_
200  ).map(lambda pair: ([0] if pair[0] else []) + pair[1])
201  return data_dims_.flatmap(lambda data_dims: st.tuples(
202  arrays(data_dims, dtype, elements),
203  segment_generator(data_dims[0], is_sorted=is_sorted),
204  ))
205 
206 
207 def lengths_tensor(min_segments=None, max_segments=None, *args, **kwargs):
208  gen = functools.partial(
209  lengths, min_segments=min_segments, max_segments=max_segments)
210  return segmented_tensor(*args, segment_generator=gen, **kwargs)
211 
212 
213 def sparse_segmented_tensor(min_dim=1, max_dim=4, dtype=np.float32,
214  is_sorted=True, elements=None, allow_empty=False,
215  segment_generator=segment_ids, itype=np.int64,
216  **kwargs):
217  gen_empty = st.booleans() if allow_empty else st.just(False)
218  data_dims_ = st.lists(dims(**kwargs), min_size=min_dim, max_size=max_dim)
219  all_dims_ = st.tuples(gen_empty, data_dims_).flatmap(
220  lambda pair: st.tuples(
221  st.just(pair[1]),
222  (st.integers(min_value=1, max_value=pair[1][0]) if not pair[0]
223  else st.just(0)),
224  ))
225  return all_dims_.flatmap(lambda dims: st.tuples(
226  arrays(dims[0], dtype, elements),
227  arrays(dims[1], dtype=itype, elements=st.integers(
228  min_value=0, max_value=dims[0][0] - 1)),
229  segment_generator(dims[1], is_sorted=is_sorted),
230  ))
231 
232 
233 def sparse_lengths_tensor(**kwargs):
234  return sparse_segmented_tensor(segment_generator=lengths, **kwargs)
235 
236 
237 def tensors(n, min_dim=1, max_dim=4, dtype=np.float32, elements=None, **kwargs):
238  dims_ = st.lists(dims(**kwargs), min_size=min_dim, max_size=max_dim)
239  return dims_.flatmap(
240  lambda dims: st.lists(
241  arrays(dims, dtype, elements),
242  min_size=n,
243  max_size=n))
244 
245 
246 def tensors1d(n, min_len=1, max_len=64, dtype=np.float32, elements=None):
247  return tensors(
248  n, 1, 1, dtype, elements, min_value=min_len, max_value=max_len
249  )
250 
251 
252 cpu_do = caffe2_pb2.DeviceOption()
253 gpu_do = caffe2_pb2.DeviceOption(device_type=caffe2_pb2.CUDA)
254 device_options = [cpu_do] + ([gpu_do] if workspace.has_gpu_support else [])
255 # Include device option for each GPU
256 expanded_device_options = [cpu_do] + (
257  [caffe2_pb2.DeviceOption(device_type=caffe2_pb2.CUDA, cuda_gpu_id=i)
258  for i in range(workspace.NumCudaDevices())]
259  if workspace.has_gpu_support else [])
260 
261 
262 def device_checker_device_options():
263  return st.just(device_options)
264 
265 
266 def gradient_checker_device_option():
267  return st.sampled_from(device_options)
268 
269 
270 gcs = dict(
271  gc=gradient_checker_device_option(),
272  dc=device_checker_device_options()
273 )
274 
275 gcs_cpu_only = dict(gc=st.sampled_from([cpu_do]), dc=st.just([cpu_do]))
276 gcs_gpu_only = dict(gc=st.sampled_from([gpu_do]), dc=st.just([gpu_do]))
277 
278 
279 @contextlib.contextmanager
280 def temp_workspace(name=b"temp_ws"):
281  old_ws_name = workspace.CurrentWorkspace()
282  workspace.SwitchWorkspace(name, True)
283  yield
284  workspace.ResetWorkspace()
285  workspace.SwitchWorkspace(old_ws_name)
286 
287 
288 def runOpBenchmark(
289  device_option,
290  op,
291  inputs,
292  input_device_options=None,
293  iterations=10,
294 ):
295  if input_device_options is None:
296  input_device_options = {}
297  op = copy.deepcopy(op)
298  op.device_option.CopyFrom(device_option)
299  net = caffe2_pb2.NetDef()
300  net.op.extend([op])
301  net.name = op.name if op.name else "test"
302 
303  with temp_workspace():
304  for (n, b) in zip(op.input, inputs):
305  workspace.FeedBlob(
306  n,
307  b,
308  device_option=input_device_options.get(n, device_option)
309  )
310  workspace.CreateNet(net)
311  ret = workspace.BenchmarkNet(net.name, 1, iterations, True)
312  return ret
313 
314 
316  """
317  A unittest.TestCase subclass with some helper functions for
318  utilizing the `hypothesis` (hypothesis.readthedocs.io) library.
319  """
320  def assertDeviceChecks(
321  self,
322  device_options,
323  op,
324  inputs,
325  outputs_to_check,
326  input_device_options=None,
327  threshold=0.01
328  ):
329  """
330  Asserts that the operator computes the same outputs, regardless of
331  which device it is executed on.
332 
333  Useful for checking the consistency of GPU and CPU
334  implementations of operators.
335 
336  Usage example:
337 
338  @given(inputs=hu.tensors(n=2), in_place=st.booleans(), **hu.gcs)
339  def test_sum(self, inputs, in_place, gc, dc):
340  op = core.CreateOperator("Sum", ["X1", "X2"],
341  ["Y" if not in_place else "X1"])
342  X1, X2 = inputs
343  self.assertDeviceChecks(dc, op, [X1, X2], [0])
344  """
346  threshold,
347  device_options=device_options
348  )
349  self.assertTrue(
350  dc.CheckSimple(op, inputs, outputs_to_check, input_device_options)
351  )
352 
354  self,
355  device_option,
356  op,
357  inputs,
358  outputs_to_check,
359  outputs_with_grads,
360  grad_ops=None,
361  threshold=0.005,
362  stepsize=0.05,
363  input_device_options=None,
364  ):
365  """
366  Implements a standard numerical gradient checker for the operator
367  in question.
368 
369  Useful for checking the consistency of the forward and
370  backward implementations of operators.
371 
372  Usage example:
373 
374  @given(inputs=hu.tensors(n=2), in_place=st.booleans(), **hu.gcs)
375  def test_sum(self, inputs, in_place, gc, dc):
376  op = core.CreateOperator("Sum", ["X1", "X2"],
377  ["Y" if not in_place else "X1"])
378  X1, X2 = inputs
379  self.assertGradientChecks(gc, op, [X1, X2], 0, [0])
380  """
382  stepsize=stepsize,
383  threshold=threshold,
384  device_option=device_option,
385  workspace_name=str(device_option),
386  )
387  res, grad, grad_estimated = gc.CheckSimple(
388  op, inputs, outputs_to_check, outputs_with_grads,
389  grad_ops=grad_ops,
390  input_device_options=input_device_options
391  )
392  self.assertEqual(grad.shape, grad_estimated.shape)
393  self.assertTrue(
394  res,
395  "Gradient check failed for input " + str(op.input[outputs_to_check])
396  )
397 
398  def _assertGradReferenceChecks(
399  self,
400  op,
401  inputs,
402  ref_outputs,
403  output_to_grad,
404  grad_reference,
405  threshold=1e-4,
406  ):
407  grad_blob_name = output_to_grad + '_grad'
408  grad_ops, grad_map = core.GradientRegistry.GetBackwardPass(
409  [op], {output_to_grad: grad_blob_name})
410  output_grad = workspace.FetchBlob(output_to_grad)
411  grad_ref_outputs = grad_reference(output_grad, ref_outputs, inputs)
412  workspace.FeedBlob(grad_blob_name, workspace.FetchBlob(output_to_grad))
413  workspace.RunOperatorsOnce(grad_ops)
414 
415  self.assertEqual(len(grad_ref_outputs), len(inputs))
416  for (n, ref) in zip(op.input, grad_ref_outputs):
417  grad_names = grad_map.get(n)
418  if not grad_names:
419  # no grad for this input
420  self.assertIsNone(ref)
421  else:
422  if isinstance(grad_names, core.BlobReference):
423  # dense gradient
424  ref_vals = ref
425  ref_indices = None
426  val_name = grad_names
427  else:
428  # sparse gradient
429  ref_vals, ref_indices = ref
430  val_name = grad_names.values
431  vals = workspace.FetchBlob(str(val_name))
432  np.testing.assert_allclose(
433  vals,
434  ref_vals,
435  atol=threshold,
436  rtol=threshold,
437  err_msg='Gradient {0} (x) is not matching the reference (y)'
438  .format(val_name),
439  )
440  if ref_indices is not None:
441  indices = workspace.FetchBlob(str(grad_names.indices))
442  np.testing.assert_allclose(indices, ref_indices,
443  atol=1e-4, rtol=1e-4)
444 
445  def _assertInferTensorChecks(self, name, shapes, types, output):
446  if name not in shapes:
447  # No inferred shape or type available
448  return
449  output = workspace.FetchBlob(name)
450  if type(output) is np.ndarray:
451  if output.dtype == np.dtype('float64'):
452  correct_type = caffe2_pb2.TensorProto.DOUBLE
453  elif output.dtype == np.dtype('float32'):
454  correct_type = caffe2_pb2.TensorProto.FLOAT
455  elif output.dtype == np.dtype('int32'):
456  correct_type = caffe2_pb2.TensorProto.INT32
457  elif output.dtype == np.dtype('int64'):
458  correct_type = caffe2_pb2.TensorProto.INT64
459  else:
460  correct_type = "unknown {}".format(np.dtype)
461  else:
462  correct_type = str(type(output))
463  try:
464  np.testing.assert_array_equal(
465  np.array(shapes[name]).astype(np.int32),
466  np.array(output.shape).astype(np.int32),
467  err_msg='Shape {} mismatch: {} vs. {}'.format(
468  name,
469  shapes[name],
470  output.shape))
471  # BUG: Workspace blob type not being set correctly T16121392
472  if correct_type != caffe2_pb2.TensorProto.INT32:
473  return
474  np.testing.assert_equal(
475  types[name],
476  correct_type,
477  err_msg='Type {} mismatch: {} vs. {}'.format(
478  name, types[name], correct_type,
479  )
480  )
481  except AssertionError as e:
482  # Temporarily catch these assertion errors when validating
483  # inferred shape and type info
484  logging.warning(str(e))
485  if os.getenv('CAFFE2_ASSERT_SHAPEINFERENCE') == '1':
486  raise e
487 
489  self,
490  device_option,
491  op,
492  inputs,
493  reference,
494  input_device_options=None,
495  threshold=1e-4,
496  output_to_grad=None,
497  grad_reference=None,
498  atol=None,
499  outputs_to_check=None,
500  ):
501  """
502  This runs the reference Python function implementation
503  (effectively calling `reference(*inputs)`, and compares that
504  to the output of output, with an absolute/relative tolerance
505  given by the `threshold` parameter.
506 
507  Useful for checking the implementation matches the Python
508  (typically NumPy) implementation of the same functionality.
509 
510  Usage example:
511 
512  @given(X=hu.tensor(), inplace=st.booleans(), **hu.gcs)
513  def test_softsign(self, X, inplace, gc, dc):
514  op = core.CreateOperator(
515  "Softsign", ["X"], ["X" if inplace else "Y"])
516 
517  def softsign(X):
518  return (X / (1 + np.abs(X)),)
519 
520  self.assertReferenceChecks(gc, op, [X], softsign)
521  """
522  if input_device_options is None:
523  input_device_options = {}
524 
525  op = copy.deepcopy(op)
526  op.device_option.CopyFrom(device_option)
527 
528  with temp_workspace():
529  if (len(op.input) > len(inputs)):
530  raise ValueError(
531  'must supply an input for each input on the op: %s vs %s' %
532  (op.input, inputs))
533  for (n, b) in zip(op.input, inputs):
534  workspace.FeedBlob(
535  n,
536  b,
537  device_option=input_device_options.get(n, device_option)
538  )
539  net = core.Net("opnet")
540  net.Proto().op.extend([op])
541  test_shape_inference = False
542  try:
543  (shapes, types) = workspace.InferShapesAndTypes([net])
544  test_shape_inference = True
545  except RuntimeError as e:
546  # Temporarily catch runtime errors when inferring shape
547  # and type info
548  logging.warning(str(e))
549  if os.getenv('CAFFE2_ASSERT_SHAPEINFERENCE') == '1':
550  raise e
551  workspace.RunNetOnce(net)
552  reference_outputs = reference(*inputs)
553  if not (isinstance(reference_outputs, tuple) or
554  isinstance(reference_outputs, list)):
555  raise RuntimeError(
556  "You are providing a wrong reference implementation. A "
557  "proper one should return a tuple/list of numpy arrays.")
558  if not outputs_to_check:
559  self.assertEqual(len(reference_outputs), len(op.output))
560  outputs_to_check = list(range(len(op.output)))
561  outs = []
562  for (output_index, ref) in zip(outputs_to_check, reference_outputs):
563  output_blob_name = op.output[output_index]
564  output = workspace.FetchBlob(output_blob_name)
565  if output.dtype.kind in ('S', 'O'):
566  np.testing.assert_array_equal(output, ref)
567  else:
568  if atol is None:
569  atol = threshold
570  np.testing.assert_allclose(
571  output, ref, atol=atol, rtol=threshold,
572  err_msg=(
573  'Output {0} is not matching the reference'.format(
574  output_blob_name,
575  )),
576  )
577  if test_shape_inference:
579  output_blob_name, shapes, types, output)
580  outs.append(output)
581  if grad_reference is not None:
582  assert output_to_grad is not None, \
583  "If grad_reference is set," \
584  "output_to_grad has to be set as well"
585 
586  with core.DeviceScope(device_option):
588  op, inputs, reference_outputs,
589  output_to_grad, grad_reference,
590  threshold=threshold)
591  return outs
592 
593  def assertValidationChecks(
594  self,
595  device_option,
596  op,
597  inputs,
598  validator,
599  input_device_options=None,
600  as_kwargs=True,
601  init_net=None,
602  ):
603  if input_device_options is None:
604  input_device_options = {}
605  if as_kwargs:
606  assert len(set(list(op.input) + list(op.output))) == \
607  len(op.input) + len(op.output), \
608  "in-place ops are not supported in as_kwargs mode"
609  op = copy.deepcopy(op)
610  op.device_option.CopyFrom(device_option)
611 
612  with temp_workspace():
613  for (n, b) in zip(op.input, inputs):
614  workspace.FeedBlob(
615  n,
616  b,
617  device_option=input_device_options.get(n, device_option)
618  )
619  if init_net:
620  workspace.RunNetOnce(init_net)
621  workspace.RunOperatorOnce(op)
622  outputs = [workspace.FetchBlob(n) for n in op.output]
623  if as_kwargs:
624  validator(**dict(zip(
625  list(op.input) + list(op.output), inputs + outputs)))
626  else:
627  validator(inputs=inputs, outputs=outputs)
628 
629  def assertRunOpRaises(
630  self,
631  device_option,
632  op,
633  inputs,
634  input_device_options=None,
635  exception=(Exception,),
636  regexp=None,
637  ):
638  if input_device_options is None:
639  input_device_options = {}
640 
641  op = copy.deepcopy(op)
642  op.device_option.CopyFrom(device_option)
643 
644  with temp_workspace():
645  for (n, b) in zip(op.input, inputs):
646  workspace.FeedBlob(
647  n,
648  b,
649  device_option=input_device_options.get(n, device_option)
650  )
651  if regexp is None:
652  self.assertRaises(exception, workspace.RunOperatorOnce, op)
653  else:
654  self.assertRaisesRegexp(
655  exception, regexp, workspace.RunOperatorOnce, op)
def _assertInferTensorChecks(self, name, shapes, types, output)
def _assertGradReferenceChecks(self, op, inputs, ref_outputs, output_to_grad, grad_reference, threshold=1e-4)
def assertReferenceChecks(self, device_option, op, inputs, reference, input_device_options=None, threshold=1e-4, output_to_grad=None, grad_reference=None, atol=None, outputs_to_check=None)
def assertDeviceChecks(self, device_options, op, inputs, outputs_to_check, input_device_options=None, threshold=0.01)
def assertGradientChecks(self, device_option, op, inputs, outputs_to_check, outputs_with_grads, grad_ops=None, threshold=0.005, stepsize=0.05, input_device_options=None)