Skip to content

Commit 43d5b07

Browse files
committed
Speedups for to_tensorflow
TensorFlow is designed around graphs of ops for a DNN, not eager channel shuffling and similar operations. It's much faster to do the channel shuffles in NumPy, although we still do the dtype conversion in TensorFlow.
1 parent a914e4a commit 43d5b07

3 files changed

Lines changed: 18 additions & 31 deletions

File tree

src/mss/screenshot.py

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -405,39 +405,22 @@ def to_tensorflow(
405405
# TypeErrors from tf.as_dtype are passed up to the caller.
406406
tf_dtype = tf.as_dtype(dtype)
407407

408-
# We don't need to do anything explicit about device management in TensorFlow; it handles that for us.
409-
# convert_to_tensor will always copy. We verified in __init__ that self._raw is a memoryview of unsigned bytes,
410-
# so that's what TensorFlow will take as the source format; we just give an explicit dtype for clarity.
411-
rv = tf.convert_to_tensor(self._raw, dtype=tf.uint8)
412-
rv = tf.reshape(rv, (self.height, self.width, 4))
413-
414-
if channels == "BGRA":
415-
pass
416-
elif channels == "BGR":
417-
rv = rv[:, :, :3]
418-
elif channels == "RGB":
419-
rv = tf.gather(rv, [2, 1, 0], axis=2)
420-
elif channels == "RGBA":
421-
rv = tf.gather(rv, [2, 1, 0, 3], axis=2)
422-
else:
423-
msg = 'Channels must be "BGRA", "BGR", "RGB", or "RGBA"'
424-
raise ValueError(msg)
408+
# Whether we're sending to a CPU or GPU, the channels and layout conversion are much faster in NumPy. I suspect
409+
# this is because doing this in TensorFlow eager mode materializes the tensor at each operation, but that's just
410+
# a guess, and putting the channel shuffles in a tf.function didn't help. It's still best (especially on GPU)
411+
# to do the dtype conversion in TensorFlow.
412+
ndarray = self.to_numpy(channels=channels, layout=layout)
425413

426-
if layout == "HWC":
427-
pass
428-
elif layout == "CHW":
429-
rv = tf.transpose(rv, perm=(2, 0, 1))
430-
else:
431-
msg = 'Layout must be "HWC" or "CHW"'
432-
raise ValueError(msg)
414+
# We don't need to do anything explicit about device management in TensorFlow; it handles that for us.
415+
# convert_to_tensor will always copy.
416+
rv = tf.convert_to_tensor(ndarray)
433417

434-
# Do the conversion last to save memory bandwidth during channel shuffles.
435418
if tf_dtype != tf.uint8:
419+
# I have no idea why, but doing the cast here instead of in convert_to_tensor is significantly faster,
420+
# especially on the GPU.
436421
rv = tf.cast(rv, dtype=tf_dtype)
437422
if tf_dtype.is_floating:
438-
# TensorFlow's implicit dtype conversion rules are not trivial. We use an explicit dtype on both sides
439-
# instead, by making a tf.constant.
440-
rv = rv / tf.constant(255.0, dtype=tf_dtype)
423+
rv /= 255.0
441424

442425
return rv
443426

src/tests/third_party/array_frameworks/test_tensorflow_method.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ def test_to_tensorflow_permutations(framework_test_image: ScreenShot, channels:
4545
bfloat16_target = uint8_target.astype(np.float32) / 255.0
4646

4747
uint8_result = framework_test_image.to_tensorflow(channels=channels, layout=layout, dtype="uint8") # type: ignore[arg-type]
48+
assert uint8_result.dtype == tf.uint8
4849
assert np.array_equal(uint8_result.numpy(), uint8_target)
4950

5051
bfloat16_result = framework_test_image.to_tensorflow(channels=channels, layout=layout, dtype="bfloat16") # type: ignore[arg-type]
52+
assert bfloat16_result.dtype == tf.bfloat16
5153
assert np.allclose(bfloat16_result.numpy(), bfloat16_target, rtol=0, atol=1 / 512.0)

src/tests/third_party/array_frameworks/test_torch_method.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def test_to_torch_permutations(framework_test_image: ScreenShot, channels: str,
4141
if cuda and not torch.cuda.is_available():
4242
# The CUDA versions won't be run in CI/CD, but it's still worth checking them on developers' machines if they
4343
# happen to have PyTorch with CUDA support.
44-
pytest.skip()
44+
pytest.skip("CUDA is not available")
4545

4646
uint8_target = reordered_test_image(channels=channels, layout=layout)
4747
bfloat16_target = uint8_target.astype(np.float32) / 255.0
@@ -52,6 +52,7 @@ def test_to_torch_permutations(framework_test_image: ScreenShot, channels: str,
5252
dtype=torch.uint8,
5353
device="cuda" if cuda else "cpu",
5454
)
55+
assert uint8_result.dtype == torch.uint8
5556
assert np.array_equal(uint8_result.cpu().numpy(), uint8_target)
5657

5758
bfloat16_result = framework_test_image.to_torch(
@@ -60,7 +61,8 @@ def test_to_torch_permutations(framework_test_image: ScreenShot, channels: str,
6061
dtype=torch.bfloat16,
6162
device="cuda" if cuda else "cpu",
6263
)
64+
assert bfloat16_result.dtype == torch.bfloat16
6365
# We have to explicitly cast back to float32 for the comparison, because PyTorch won't directly convert bfloat16 to
6466
# NumPy.
65-
bfloat16_result = bfloat16_result.to(torch.float32)
66-
assert np.allclose(bfloat16_result.cpu().numpy(), bfloat16_target, rtol=0, atol=1 / 512.0)
67+
bfloat16_result_f32 = bfloat16_result.to(torch.float32)
68+
assert np.allclose(bfloat16_result_f32.cpu().numpy(), bfloat16_target, rtol=0, atol=1 / 512.0)

0 commit comments

Comments
 (0)