From b3b36d7a83e5f60bddc4742de9ddee780a97909c Mon Sep 17 00:00:00 2001 From: Yannick Assogba Date: Wed, 8 Jul 2020 14:57:18 -0400 Subject: [PATCH 1/4] remove noop from extract_op --- tfjs-core/scripts/extract_op.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tfjs-core/scripts/extract_op.ts b/tfjs-core/scripts/extract_op.ts index 1b2fba037f9..61e29a65aaf 100644 --- a/tfjs-core/scripts/extract_op.ts +++ b/tfjs-core/scripts/extract_op.ts @@ -1,5 +1,4 @@ import * as argparse from 'argparse'; -import {execSync} from 'child_process'; import * as path from 'path'; import {FunctionDeclaration, ImportDeclaration, Project, SourceFile, VariableStatement} from 'ts-morph'; @@ -85,12 +84,6 @@ async function moveToNewFile( newOpFile.fixUnusedIdentifiers(); await newOpFile.save(); - // make a test file - // create a test file - const testFilePath = `src/ops/${args.op}_test.ts`; - const command = `touch ${testFilePath}`; - execSync(command); - // Add export to ops file. const opsExportFile = project.getSourceFile('src/ops/ops.ts'); opsExportFile.addExportDeclaration({ From 7ca01afdc912d3e986caea0ab56d1247f204b10e Mon Sep 17 00:00:00 2001 From: Yannick Assogba Date: Wed, 8 Jul 2020 15:57:13 -0400 Subject: [PATCH 2/4] modularize slice ops --- tfjs-core/src/gradients/Slice_grad.ts | 46 +++ tfjs-core/src/kernel_names.ts | 7 + tfjs-core/src/ops/ops.ts | 6 +- tfjs-core/src/ops/slice.ts | 131 +------ tfjs-core/src/ops/slice1d.ts | 39 ++ tfjs-core/src/ops/slice1d_test.ts | 70 ++++ tfjs-core/src/ops/slice2d.ts | 40 ++ tfjs-core/src/ops/slice2d_test.ts | 227 ++++++++++++ tfjs-core/src/ops/slice3d.ts | 40 ++ tfjs-core/src/ops/slice3d_test.ts | 54 +++ tfjs-core/src/ops/slice4d.ts | 40 ++ tfjs-core/src/ops/slice4d_test.ts | 58 +++ tfjs-core/src/ops/slice_test.ts | 473 ++++-------------------- tfjs-core/src/ops/slice_util.ts | 39 ++ tfjs-core/src/register_all_gradients.ts | 2 + tfjs-core/src/tests.ts | 4 + 16 files changed, 758 insertions(+), 518 deletions(-) create mode 100644 tfjs-core/src/gradients/Slice_grad.ts create mode 100644 tfjs-core/src/ops/slice1d.ts create mode 100644 tfjs-core/src/ops/slice1d_test.ts create mode 100644 tfjs-core/src/ops/slice2d.ts create mode 100644 tfjs-core/src/ops/slice2d_test.ts create mode 100644 tfjs-core/src/ops/slice3d.ts create mode 100644 tfjs-core/src/ops/slice3d_test.ts create mode 100644 tfjs-core/src/ops/slice4d.ts create mode 100644 tfjs-core/src/ops/slice4d_test.ts diff --git a/tfjs-core/src/gradients/Slice_grad.ts b/tfjs-core/src/gradients/Slice_grad.ts new file mode 100644 index 00000000000..f4c3cc91a8b --- /dev/null +++ b/tfjs-core/src/gradients/Slice_grad.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2020 Google LLC. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import {Slice, SliceAttrs} from '../kernel_names'; +import {GradConfig, NamedAttrMap} from '../kernel_registry'; +import {pad} from '../ops/pad'; +import {parseSliceParams} from '../ops/slice_util'; +import {Tensor} from '../tensor'; + +export const sliceGradConfig: GradConfig = { + kernelName: Slice, + inputsToSave: ['x'], + gradFunc: (dy: Tensor, saved: Tensor[], attrs: NamedAttrMap) => { + const [x] = saved; + const {begin, size} = attrs as {} as SliceAttrs; + + const inputShape = x.shape; + const [begin_, size_] = parseSliceParams(x, begin, size); + + // Create an Nx2 padding where the first column represents how many + // zeros are prepended (at start) for each dimension, and the second + // column indicates how many zeros are appended (at end). + + // The number of zeros to append is the shape of the input + // elementwise-subtracted by both the begin vector and sizes vector. + const paddings: Array<[number, number]> = []; + for (let i = 0; i < dy.rank; i++) { + paddings.push([begin_[i], inputShape[i] - begin_[i] - size_[i]]); + } + return {x: () => pad(dy, paddings)}; + } +}; diff --git a/tfjs-core/src/kernel_names.ts b/tfjs-core/src/kernel_names.ts index 751c6d45f1a..1bdd824b2fa 100644 --- a/tfjs-core/src/kernel_names.ts +++ b/tfjs-core/src/kernel_names.ts @@ -526,6 +526,13 @@ export type SelectV2Inputs = Pick; export const Selu = 'Selu'; export type SeluInputs = Pick; +export const Slice = 'Slice'; +export type SliceInputs = Pick; +export interface SliceAttrs { + begin: number|number[]; + size: number|number[]; +} + export const Sign = 'Sign'; export type SignInputs = UnaryInputs; diff --git a/tfjs-core/src/ops/ops.ts b/tfjs-core/src/ops/ops.ts index 50fee3c0e72..303184572a9 100644 --- a/tfjs-core/src/ops/ops.ts +++ b/tfjs-core/src/ops/ops.ts @@ -116,6 +116,11 @@ export {reverse4d} from './reverse_4d'; export {selu} from './selu'; export {separableConv2d} from './separable_conv2d'; export {sign} from './sign'; +export {slice} from './slice'; +export {slice1d} from './slice1d'; +export {slice2d} from './slice2d'; +export {slice3d} from './slice3d'; +export {slice4d} from './slice4d'; export {spaceToBatchND} from './space_to_batch_nd'; export {split} from './split'; export {square} from './square'; @@ -132,7 +137,6 @@ export {where} from './where'; export {whereAsync} from './where_async'; export * from './boolean_mask'; -export * from './slice'; export * from './unary_ops'; export * from './compare'; export * from './binary_ops'; diff --git a/tfjs-core/src/ops/slice.ts b/tfjs-core/src/ops/slice.ts index 1ba1d766da5..bf0a44d87a2 100644 --- a/tfjs-core/src/ops/slice.ts +++ b/tfjs-core/src/ops/slice.ts @@ -15,75 +15,17 @@ * ============================================================================= */ -import {ENGINE} from '../engine'; -import {Tensor, Tensor1D, Tensor2D, Tensor3D, Tensor4D} from '../tensor'; +import {ENGINE, ForwardFunc} from '../engine'; +import {Slice, SliceAttrs, SliceInputs} from '../kernel_names'; +import {NamedAttrMap} from '../kernel_registry'; +import {Tensor} from '../tensor'; +import {NamedTensorMap} from '../tensor_types'; import {convertToTensor} from '../tensor_util_env'; import {Rank, TensorLike} from '../types'; -import * as util from '../util'; import {op} from './operation'; -import {pad} from './pad'; import * as slice_util from './slice_util'; -/** - * Extracts a 1D slice from 1D array starting at coordinates `begin` and is - * of length `size`. See `slice` for details. - */ -function slice1d_( - x: Tensor1D|TensorLike, begin: number, size: number): Tensor1D { - const $x = convertToTensor(x, 'x', 'slice1d'); - util.assert( - $x.rank === 1, - () => - `slice1d expects a rank-1 tensor, but got a rank-${$x.rank} tensor`); - return slice($x, [begin], [size]); -} - -/** - * Extracts a 2D slice from a 2D array starting at coordinates `begin` and - * is of size `size`. See `slice` for details. - */ -function slice2d_( - x: Tensor2D|TensorLike, begin: [number, number], - size: [number, number]): Tensor2D { - const $x = convertToTensor(x, 'x', 'slice2d'); - util.assert( - $x.rank === 2, - () => - `slice2d expects a rank-2 tensor, but got a rank-${$x.rank} tensor`); - return slice($x, begin, size); -} - -/** - * Extracts a 3D slice from a 3D array starting at coordinates `begin` and - * is of size `size`. See `slice` for details. - */ -function slice3d_( - x: Tensor3D|TensorLike, begin: [number, number, number], - size: [number, number, number]): Tensor3D { - const $x = convertToTensor(x, 'x', 'slice3d'); - util.assert( - $x.rank === 3, - () => - `slice3d expects a rank-3 tensor, but got a rank-${$x.rank} tensor`); - return slice($x, begin, size); -} - -/** - * Extracts a 4D slice from a 4D array starting at coordinates `begin` and - * is of size `size`. See `slice` for details. - */ -function slice4d_( - x: Tensor4D|TensorLike, begin: [number, number, number, number], - size: [number, number, number, number]): Tensor4D { - const $x = convertToTensor(x, 'x', 'slice4d'); - util.assert( - $x.rank === 4, - () => - `slice4d expects a rank-4 tensor, but got a rank-${$x.rank} tensor`); - return slice($x, begin, size); -} - /** * Extracts a slice from a `tf.Tensor` starting at coordinates `begin` * and is of size `size`. @@ -124,63 +66,20 @@ function slice_>( if ($x.rank === 0) { throw new Error('Slicing scalar is not possible'); } - // The following logic allows for more ergonomic calls. - let begin_: number[]; - if (typeof begin === 'number') { - begin_ = [begin, ...new Array($x.rank - 1).fill(0)]; - } else if (begin.length < $x.rank) { - begin_ = begin.concat(new Array($x.rank - begin.length).fill(0)); - } else { - begin_ = begin.slice(); - } - begin_.forEach(d => { - util.assert( - d !== -1, () => 'slice() does not support negative begin indexing.'); - }); - let size_: number[]; - if (size == null) { - size_ = new Array($x.rank).fill(-1); - } else if (typeof size === 'number') { - size_ = [size, ...new Array($x.rank - 1).fill(-1)]; - } else if (size.length < $x.rank) { - size_ = size.concat(new Array($x.rank - size.length).fill(-1)); - } else { - size_ = size; - } - size_ = size_.map((d, i) => { - if (d >= 0) { - return d; - } else { - util.assert( - d === -1, - () => `Negative size values should be exactly -1 but got ` + - `${d} for the slice() size at index ${i}.`); - return $x.shape[i] - begin_[i]; - } - }); + const [begin_, size_] = slice_util.parseSliceParams($x, begin, size); slice_util.assertParamsValid($x, begin_, size_); - const inputShape = $x.shape; - const grad = (dy: T) => { - // Create an Nx2 padding where the first column represents how many - // zeros are prepended (at start) for each dimension, and the second - // column indicates how many zeros are appended (at end). - // The number of zeros to append is the shape of the input - // elementwise-subtracted by both the begin vector and sizes vector. - const paddings: Array<[number, number]> = []; - for (let i = 0; i < dy.rank; i++) { - paddings.push([begin_[i], inputShape[i] - begin_[i] - size_[i]]); - } - return {x: () => pad(dy, paddings)}; + const forward: ForwardFunc = (backend, save) => { + save([$x]); + return backend.slice($x, begin_, size_); }; - const attrs = {begin: begin_, size: size_}; + + const inputs: SliceInputs = {x: $x}; + const attrs: SliceAttrs = {begin, size}; + return ENGINE.runKernelFunc( - backend => backend.slice($x, begin_, size_), {x: $x}, grad, 'Slice', - attrs); + forward, inputs as {} as NamedTensorMap, null /* grad */, Slice, + attrs as {} as NamedAttrMap) as T; } export const slice = op({slice_}); -export const slice1d = op({slice1d_}); -export const slice2d = op({slice2d_}); -export const slice3d = op({slice3d_}); -export const slice4d = op({slice4d_}); diff --git a/tfjs-core/src/ops/slice1d.ts b/tfjs-core/src/ops/slice1d.ts new file mode 100644 index 00000000000..d4bb3e05f17 --- /dev/null +++ b/tfjs-core/src/ops/slice1d.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright 2018 Google LLC. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import {Tensor1D} from '../tensor'; +import {convertToTensor} from '../tensor_util_env'; +import {TensorLike} from '../types'; +import * as util from '../util'; + +import {op} from './operation'; +import {slice} from './slice'; + +/** + * Extracts a 1D slice from 1D array starting at coordinates `begin` and is + * of length `size`. See `slice` for details. + */ +function slice1d_( + x: Tensor1D|TensorLike, begin: number, size: number): Tensor1D { + const $x = convertToTensor(x, 'x', 'slice1d'); + util.assert( + $x.rank === 1, + () => + `slice1d expects a rank-1 tensor, but got a rank-${$x.rank} tensor`); + return slice($x, [begin], [size]); +} +export const slice1d = op({slice1d_}); diff --git a/tfjs-core/src/ops/slice1d_test.ts b/tfjs-core/src/ops/slice1d_test.ts new file mode 100644 index 00000000000..156d41b1a94 --- /dev/null +++ b/tfjs-core/src/ops/slice1d_test.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright 2020 Google LLC. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as tf from '../index'; +import {ALL_ENVS, describeWithFlags} from '../jasmine_util'; +import {expectArraysClose} from '../test_util'; + +describeWithFlags('slice1d', ALL_ENVS, () => { + it('slices 1x1 into 1x1 (effectively a copy)', async () => { + const a = tf.tensor1d([5]); + const result = tf.slice1d(a, 0, 1); + + expect(result.shape).toEqual([1]); + expectArraysClose(await result.data(), 5); + }); + + it('slices 5x1 into shape 2x1 starting at 3', async () => { + const a = tf.tensor1d([1, 2, 3, 4, 5]); + const result = tf.slice1d(a, 3, 2); + + expect(result.shape).toEqual([2]); + expectArraysClose(await result.data(), [4, 5]); + }); + + it('slices 5x1 into shape 3x1 starting at 1', async () => { + const a = tf.tensor1d([1, 2, 3, 4, 5]); + const result = tf.slice1d(a, 1, 3); + + expect(result.shape).toEqual([3]); + expectArraysClose(await result.data(), [2, 3, 4]); + }); + + it('grad', async () => { + const a = tf.tensor1d([1, 2, 3, 4, 5]); + const dy = tf.tensor1d([10, 100]); + const da = tf.grad((a: tf.Tensor1D) => tf.slice1d(a, 1, 2))(a, dy); + expect(da.shape).toEqual([5]); + expectArraysClose(await da.data(), [0, 10, 100, 0, 0]); + }); + + it('gradient with clones', async () => { + const a = tf.tensor1d([1, 2, 3, 4, 5]); + const dy = tf.tensor1d([10, 100]); + const da = + tf.grad((a: tf.Tensor1D) => tf.slice1d(a.clone(), 1, 2).clone())(a, dy); + expect(da.shape).toEqual([5]); + expectArraysClose(await da.data(), [0, 10, 100, 0, 0]); + }); + + it('accepts a tensor-like object', async () => { + const a = [5]; + const result = tf.slice1d(a, 0, 1); + expect(result.shape).toEqual([1]); + expectArraysClose(await result.data(), 5); + }); +}); diff --git a/tfjs-core/src/ops/slice2d.ts b/tfjs-core/src/ops/slice2d.ts new file mode 100644 index 00000000000..569f76bf315 --- /dev/null +++ b/tfjs-core/src/ops/slice2d.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright 2018 Google LLC. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import {Tensor2D} from '../tensor'; +import {convertToTensor} from '../tensor_util_env'; +import {TensorLike} from '../types'; +import * as util from '../util'; + +import {op} from './operation'; +import {slice} from './slice'; + +/** + * Extracts a 2D slice from a 2D array starting at coordinates `begin` and + * is of size `size`. See `slice` for details. + */ +function slice2d_( + x: Tensor2D|TensorLike, begin: [number, number], + size: [number, number]): Tensor2D { + const $x = convertToTensor(x, 'x', 'slice2d'); + util.assert( + $x.rank === 2, + () => + `slice2d expects a rank-2 tensor, but got a rank-${$x.rank} tensor`); + return slice($x, begin, size); +} +export const slice2d = op({slice2d_}); diff --git a/tfjs-core/src/ops/slice2d_test.ts b/tfjs-core/src/ops/slice2d_test.ts new file mode 100644 index 00000000000..4f963645da4 --- /dev/null +++ b/tfjs-core/src/ops/slice2d_test.ts @@ -0,0 +1,227 @@ +/** + * @license + * Copyright 2020 Google LLC. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as tf from '../index'; +import {ALL_ENVS, describeWithFlags} from '../jasmine_util'; +import {expectArraysClose} from '../test_util'; +import {Rank} from '../types'; + +describeWithFlags('slice2d', ALL_ENVS, () => { + it('slicing a 1x1 from a 1x1 returns a 1x1', () => { + const a = tf.tensor2d([0], [1, 1]); + const b = tf.slice2d(a, [0, 0], [1, 1]); + expect(b.shape).toEqual([1, 1]); + }); + + it('returns a tensor of slice size', () => { + const a = tf.zeros([100, 100]); + const b = tf.slice2d(a, [0, 0], [12, 34]); + expect(b.shape).toEqual([12, 34]); + }); + + it('returns the upper-left submatrix when begin is [0, 0]', async () => { + const a = tf.randomUniform([10, 10], -1, 1); + const b = tf.slice2d(a, [0, 0], [2, 2]); + const aValues = await a.data(); + + expectArraysClose( + await b.data(), [aValues[0], aValues[1], aValues[10], aValues[11]]); + }); + + it('returns the rectangle specified', async () => { + const a = tf.tensor2d([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], [4, 3]); + const b = tf.slice2d(a, [1, 1], [3, 2]); + + expectArraysClose(await b.data(), [5, 6, 8, 9, 11, 12]); + }); + + it('throws when requesting out of bounds slice', () => { + const a = tf.tensor2d([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], [4, 3]); + expect(() => tf.slice2d(a, [1, 1], [10, 10])).toThrowError(); + }); + + it('grad', async () => { + const a = tf.tensor2d([[1, 2, 3], [4, 5, 6]]); + const dy = tf.tensor2d([[20], [50]]); + const da = + tf.grad((x: tf.Tensor2D) => tf.slice2d(a, [0, 1], [2, 1]))(a, dy); + expect(da.shape).toEqual([2, 3]); + expectArraysClose(await da.data(), [0, 20, 0, 0, 50, 0]); + }); + + it('accepts a tensor-like object', () => { + const a = [[0]]; // 1x1 + const b = tf.slice2d(a, [0, 0], [1, 1]); + expect(b.shape).toEqual([1, 1]); + }); + + it('slice an already sliced tensor, first was not continous', async () => { + const a = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + ]; // 3x4. + const b = tf.slice(a, [0, 1]); + const c = tf.slice(b, [1, 1], [1, 1]); + expect(c.shape).toEqual([1, 1]); + expectArraysClose(await c.data(), [7]); + }); + + it('slice an already sliced tensor, first was continous', async () => { + const a = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + ]; // 3x4. + const b = tf.slice(a, [1, 0]); + const c = tf.slice(b, [1, 0]); + expect(c.shape).toEqual([1, 4]); + expectArraysClose(await c.data(), [9, 10, 11, 12]); + }); + + it('slice an already sliced tensor and do async read', async () => { + const a = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + ]; // 3x4. + const b = tf.slice(a, [0, 1]); + const c = tf.slice(b, [1, 1], [1, 1]); + expect(c.shape).toEqual([1, 1]); + expectArraysClose(await c.data(), new Float32Array([7])); + }); + + it('square a sliced texture, followed by non-sliced texture of same shape', + async () => { // Tests collisions in the shader cache. + // Make a 2x3 tensor, upload to gpu and reshape to 3x2. + const input = tf.tensor([[1, 2, 3], [4, 5, 6]]).abs().as2D(3, 2); + const slicedInput = tf.slice(input, [0, 0], [3, 2]); + // First square program takes the sliced input. + const a = slicedInput.square(); + expectArraysClose(await a.data(), [1, 4, 9, 16, 25, 36]); + // Second square program takes the non-sliced input. + const b = tf.square(input); + expectArraysClose(await b.data(), [1, 4, 9, 16, 25, 36]); + }); + + it('square a non-sliced texture, followed by a sliced texture of same shape', + async () => { // Tests collisions in the shader cache. + // Make a 2x3 tensor, upload to gpu and reshape to 3x2. + const input = tf.tensor([[1, 2, 3], [4, 5, 6]]).abs().as2D(3, 2); + // Make a sliced version of the same tensor with the same shape. + const slicedInput = tf.slice(input, [0, 0], [3, 2]); + // First square program takes the non-sliced input. + const a = input.square(); + expectArraysClose(await a.data(), [1, 4, 9, 16, 25, 36]); + // Second square program takes the sliced input. + const b = tf.square(slicedInput); + expectArraysClose(await b.data(), [1, 4, 9, 16, 25, 36]); + }); + + it('slice a tensor and do async read', async () => { + const a = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + ]; // 3x4. + const b = tf.slice(a, [0, 1], [3, 2]); + expect(b.shape).toEqual([3, 2]); + const vals = await b.data(); + expectArraysClose(vals, new Float32Array([2, 3, 6, 7, 10, 11])); + }); + + it('flatten a sliced tensor that was continuous in memory', async () => { + const a = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + ]; // 3x4. + const b = tf.slice(a, [1, 0]).flatten(); + expect(b.shape).toEqual([8]); + expectArraysClose(await b.data(), [5, 6, 7, 8, 9, 10, 11, 12]); + }); + + it('slice a tensor that was not continuous in memory', async () => { + const a = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + ]; // 3x4. + const b = tf.slice(a, [0, 1]); + expect(b.shape).toEqual([3, 3]); + expectArraysClose(await b.data(), [2, 3, 4, 6, 7, 8, 10, 11, 12]); + }); + + it('flatten a sliced tensor that was not continuous in memory', async () => { + const a = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + ]; // 3x4. + const b = tf.slice(a, [0, 1]).flatten(); + expect(b.shape).toEqual([9]); + expectArraysClose(await b.data(), [2, 3, 4, 6, 7, 8, 10, 11, 12]); + }); + + it('flatten a sliced tensor not continuous in memory and run program', + async () => { + const a = [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + ]; // 3x4. + const b = tf.slice(a, [0, 1]).flatten(); + const c = tf.square(b); + expectArraysClose(await c.data(), [4, 9, 16, 36, 49, 64, 100, 121, 144]); + }); + + it('reshape a sliced 1d into a 2d tensor', async () => { + const a = [1, 2, 3, 4, 5]; + const b = tf.slice(a, 1).as2D(2, 2); + expect(b.shape).toEqual([2, 2]); + expectArraysClose(await b.data(), [2, 3, 4, 5]); + }); + + it('reshape a sliced 1d into a 2d tensor and run program', async () => { + const a = [1, 2, 3, 4, 5]; + const b = tf.slice(a, 1).as2D(2, 2).square(); + expect(b.shape).toEqual([2, 2]); + expectArraysClose(await b.data(), [4, 9, 16, 25]); + }); + + it('broadcast the original with the sliced tensor', async () => { + const a = [[1, 2], [3, 4]]; + const b = tf.slice(a, [0, 1]); + const c = tf.add(a, b); + expect(c.shape).toEqual([2, 2]); + expectArraysClose(await c.data(), [3, 4, 7, 8]); + }); + + it('zero-sized slice out of a non-zero sized tensor', async () => { + const a = tf.zeros([4, 2]); + const res = tf.slice(a, [0, 0], [0, 2]); + expect(res.shape).toEqual([0, 2]); + expectArraysClose(await res.data(), []); + }); + + it('zero-sized slice out of a zero-sized tensor', async () => { + const a = tf.zeros([0, 4]); + const res = tf.slice(a, [0, 1], [0, 3]); + expect(res.shape).toEqual([0, 3]); + expectArraysClose(await res.data(), []); + }); +}); diff --git a/tfjs-core/src/ops/slice3d.ts b/tfjs-core/src/ops/slice3d.ts new file mode 100644 index 00000000000..5cc276637f2 --- /dev/null +++ b/tfjs-core/src/ops/slice3d.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright 2018 Google LLC. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import {Tensor3D} from '../tensor'; +import {convertToTensor} from '../tensor_util_env'; +import {TensorLike} from '../types'; +import * as util from '../util'; + +import {op} from './operation'; +import {slice} from './slice'; + +/** + * Extracts a 3D slice from a 3D array starting at coordinates `begin` and + * is of size `size`. See `slice` for details. + */ +function slice3d_( + x: Tensor3D|TensorLike, begin: [number, number, number], + size: [number, number, number]): Tensor3D { + const $x = convertToTensor(x, 'x', 'slice3d'); + util.assert( + $x.rank === 3, + () => + `slice3d expects a rank-3 tensor, but got a rank-${$x.rank} tensor`); + return slice($x, begin, size); +} +export const slice3d = op({slice3d_}); diff --git a/tfjs-core/src/ops/slice3d_test.ts b/tfjs-core/src/ops/slice3d_test.ts new file mode 100644 index 00000000000..ca38dccb6ab --- /dev/null +++ b/tfjs-core/src/ops/slice3d_test.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2020 Google LLC. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as tf from '../index'; +import {ALL_ENVS, describeWithFlags} from '../jasmine_util'; +import {expectArraysClose} from '../test_util'; + +describeWithFlags('slice3d', ALL_ENVS, () => { + it('slices 1x1x1 into shape 1x1x1 (effectively a copy)', async () => { + const a = tf.tensor3d([[[5]]], [1, 1, 1]); + const result = tf.slice3d(a, [0, 0, 0], [1, 1, 1]); + + expect(result.shape).toEqual([1, 1, 1]); + expectArraysClose(await result.data(), [5]); + }); + + it('slices 2x2x2 array into 1x2x2 starting at [1, 0, 0]', async () => { + const a = tf.tensor3d([1, 2, 3, 4, 5, 6, 7, 8], [2, 2, 2]); + const result = tf.slice3d(a, [1, 0, 0], [1, 2, 2]); + + expect(result.shape).toEqual([1, 2, 2]); + expectArraysClose(await result.data(), [5, 6, 7, 8]); + }); + + it('slices 2x2x2 array into 2x1x1 starting at [0, 1, 1]', async () => { + const a = tf.tensor3d([1, 2, 3, 4, 5, 6, 7, 8], [2, 2, 2]); + const result = tf.slice3d(a, [0, 1, 1], [2, 1, 1]); + + expect(result.shape).toEqual([2, 1, 1]); + expectArraysClose(await result.data(), [4, 8]); + }); + + it('accepts a tensor-like object', async () => { + const a = [[[5]]]; // 1x1x1 + const result = tf.slice3d(a, [0, 0, 0], [1, 1, 1]); + + expect(result.shape).toEqual([1, 1, 1]); + expectArraysClose(await result.data(), [5]); + }); +}); diff --git a/tfjs-core/src/ops/slice4d.ts b/tfjs-core/src/ops/slice4d.ts new file mode 100644 index 00000000000..3815dc7c13c --- /dev/null +++ b/tfjs-core/src/ops/slice4d.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright 2018 Google LLC. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import {Tensor4D} from '../tensor'; +import {convertToTensor} from '../tensor_util_env'; +import {TensorLike} from '../types'; +import * as util from '../util'; + +import {op} from './operation'; +import {slice} from './slice'; + +/** + * Extracts a 4D slice from a 4D array starting at coordinates `begin` and + * is of size `size`. See `slice` for details. + */ +function slice4d_( + x: Tensor4D|TensorLike, begin: [number, number, number, number], + size: [number, number, number, number]): Tensor4D { + const $x = convertToTensor(x, 'x', 'slice4d'); + util.assert( + $x.rank === 4, + () => + `slice4d expects a rank-4 tensor, but got a rank-${$x.rank} tensor`); + return slice($x, begin, size); +} +export const slice4d = op({slice4d_}); diff --git a/tfjs-core/src/ops/slice4d_test.ts b/tfjs-core/src/ops/slice4d_test.ts new file mode 100644 index 00000000000..be36a7c3ca0 --- /dev/null +++ b/tfjs-core/src/ops/slice4d_test.ts @@ -0,0 +1,58 @@ +/** + * @license + * Copyright 2020 Google LLC. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================================= + */ + +import * as tf from '../index'; +import {ALL_ENVS, describeWithFlags} from '../jasmine_util'; +import {expectArraysClose} from '../test_util'; + +describeWithFlags('slice4d', ALL_ENVS, () => { + it('slices 1x1x1x1 into shape 1x1x1x1 (effectively a copy)', async () => { + const a = tf.tensor4d([[[[5]]]], [1, 1, 1, 1]); + const result = tf.slice4d(a, [0, 0, 0, 0], [1, 1, 1, 1]); + + expect(result.shape).toEqual([1, 1, 1, 1]); + expectArraysClose(await result.data(), [5]); + }); + + it('slices 2x2x2x2 array into 1x2x2x2 starting at [1, 0, 0, 0]', async () => { + const a = tf.tensor4d( + [1, 2, 3, 4, 5, 6, 7, 8, 11, 22, 33, 44, 55, 66, 77, 88], + [2, 2, 2, 2], + ); + const result = tf.slice4d(a, [1, 0, 0, 0], [1, 2, 2, 2]); + + expect(result.shape).toEqual([1, 2, 2, 2]); + expectArraysClose(await result.data(), [11, 22, 33, 44, 55, 66, 77, 88]); + }); + + it('slices 2x2x2x2 array into 2x1x1x1 starting at [0, 1, 1, 1]', async () => { + const a = tf.tensor4d( + [1, 2, 3, 4, 5, 6, 7, 8, 11, 22, 33, 44, 55, 66, 77, 88], [2, 2, 2, 2]); + const result = tf.slice4d(a, [0, 1, 1, 1], [2, 1, 1, 1]); + + expect(result.shape).toEqual([2, 1, 1, 1]); + expectArraysClose(await result.data(), [8, 88]); + }); + + it('accepts a tensor-like object', async () => { + const a = [[[[5]]]]; // 1x1x1x1 + const result = tf.slice4d(a, [0, 0, 0, 0], [1, 1, 1, 1]); + + expect(result.shape).toEqual([1, 1, 1, 1]); + expectArraysClose(await result.data(), [5]); + }); +}); diff --git a/tfjs-core/src/ops/slice_test.ts b/tfjs-core/src/ops/slice_test.ts index d022d394606..cc59e2abb0d 100644 --- a/tfjs-core/src/ops/slice_test.ts +++ b/tfjs-core/src/ops/slice_test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google LLC. All Rights Reserved. + * Copyright 2018 Google LLC. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -18,333 +18,104 @@ import * as tf from '../index'; import {ALL_ENVS, describeWithFlags, SYNC_BACKEND_ENVS} from '../jasmine_util'; import {expectArraysClose} from '../test_util'; -import {Rank} from '../types'; -describeWithFlags('slice1d', ALL_ENVS, () => { - it('slices 1x1 into 1x1 (effectively a copy)', async () => { - const a = tf.tensor1d([5]); - const result = tf.slice1d(a, 0, 1); - - expect(result.shape).toEqual([1]); - expectArraysClose(await result.data(), 5); - }); - - it('slices 5x1 into shape 2x1 starting at 3', async () => { - const a = tf.tensor1d([1, 2, 3, 4, 5]); - const result = tf.slice1d(a, 3, 2); - - expect(result.shape).toEqual([2]); - expectArraysClose(await result.data(), [4, 5]); - }); - - it('slices 5x1 into shape 3x1 starting at 1', async () => { - const a = tf.tensor1d([1, 2, 3, 4, 5]); - const result = tf.slice1d(a, 1, 3); - - expect(result.shape).toEqual([3]); - expectArraysClose(await result.data(), [2, 3, 4]); - }); - - it('grad', async () => { - const a = tf.tensor1d([1, 2, 3, 4, 5]); - const dy = tf.tensor1d([10, 100]); - const da = tf.grad((a: tf.Tensor1D) => tf.slice1d(a, 1, 2))(a, dy); - expect(da.shape).toEqual([5]); - expectArraysClose(await da.data(), [0, 10, 100, 0, 0]); - }); - - it('gradient with clones', async () => { - const a = tf.tensor1d([1, 2, 3, 4, 5]); - const dy = tf.tensor1d([10, 100]); - const da = - tf.grad((a: tf.Tensor1D) => tf.slice1d(a.clone(), 1, 2).clone())(a, dy); - expect(da.shape).toEqual([5]); - expectArraysClose(await da.data(), [0, 10, 100, 0, 0]); - }); - - it('accepts a tensor-like object', async () => { - const a = [5]; - const result = tf.slice1d(a, 0, 1); - expect(result.shape).toEqual([1]); - expectArraysClose(await result.data(), 5); - }); -}); - -describeWithFlags('slice2d', ALL_ENVS, () => { - it('slicing a 1x1 from a 1x1 returns a 1x1', () => { - const a = tf.tensor2d([0], [1, 1]); - const b = tf.slice2d(a, [0, 0], [1, 1]); - expect(b.shape).toEqual([1, 1]); - }); - - it('returns a tensor of slice size', () => { - const a = tf.zeros([100, 100]); - const b = tf.slice2d(a, [0, 0], [12, 34]); - expect(b.shape).toEqual([12, 34]); - }); - - it('returns the upper-left submatrix when begin is [0, 0]', async () => { - const a = tf.randomUniform([10, 10], -1, 1); - const b = tf.slice2d(a, [0, 0], [2, 2]); - const aValues = await a.data(); - - expectArraysClose( - await b.data(), [aValues[0], aValues[1], aValues[10], aValues[11]]); - }); - - it('returns the rectangle specified', async () => { - const a = tf.tensor2d([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], [4, 3]); - const b = tf.slice2d(a, [1, 1], [3, 2]); - - expectArraysClose(await b.data(), [5, 6, 8, 9, 11, 12]); - }); - - it('throws when requesting out of bounds slice', () => { - const a = tf.tensor2d([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], [4, 3]); - expect(() => tf.slice2d(a, [1, 1], [10, 10])).toThrowError(); - }); - - it('grad', async () => { - const a = tf.tensor2d([[1, 2, 3], [4, 5, 6]]); - const dy = tf.tensor2d([[20], [50]]); - const da = - tf.grad((x: tf.Tensor2D) => tf.slice2d(a, [0, 1], [2, 1]))(a, dy); - expect(da.shape).toEqual([2, 3]); - expectArraysClose(await da.data(), [0, 20, 0, 0, 50, 0]); - }); - - it('accepts a tensor-like object', () => { - const a = [[0]]; // 1x1 - const b = tf.slice2d(a, [0, 0], [1, 1]); - expect(b.shape).toEqual([1, 1]); - }); - - it('slice an already sliced tensor, first was not continous', async () => { - const a = [ - [1, 2, 3, 4], - [5, 6, 7, 8], - [9, 10, 11, 12], - ]; // 3x4. - const b = tf.slice(a, [0, 1]); - const c = tf.slice(b, [1, 1], [1, 1]); - expect(c.shape).toEqual([1, 1]); - expectArraysClose(await c.data(), [7]); - }); - - it('slice an already sliced tensor, first was continous', async () => { - const a = [ - [1, 2, 3, 4], - [5, 6, 7, 8], - [9, 10, 11, 12], - ]; // 3x4. - const b = tf.slice(a, [1, 0]); - const c = tf.slice(b, [1, 0]); - expect(c.shape).toEqual([1, 4]); - expectArraysClose(await c.data(), [9, 10, 11, 12]); - }); - - it('slice an already sliced tensor and do async read', async () => { - const a = [ - [1, 2, 3, 4], - [5, 6, 7, 8], - [9, 10, 11, 12], - ]; // 3x4. - const b = tf.slice(a, [0, 1]); - const c = tf.slice(b, [1, 1], [1, 1]); - expect(c.shape).toEqual([1, 1]); - expectArraysClose(await c.data(), new Float32Array([7])); - }); - - it('square a sliced texture, followed by non-sliced texture of same shape', - async () => { // Tests collisions in the shader cache. - // Make a 2x3 tensor, upload to gpu and reshape to 3x2. - const input = tf.tensor([[1, 2, 3], [4, 5, 6]]).abs().as2D(3, 2); - const slicedInput = tf.slice(input, [0, 0], [3, 2]); - // First square program takes the sliced input. - const a = slicedInput.square(); - expectArraysClose(await a.data(), [1, 4, 9, 16, 25, 36]); - // Second square program takes the non-sliced input. - const b = tf.square(input); - expectArraysClose(await b.data(), [1, 4, 9, 16, 25, 36]); - }); - - it('square a non-sliced texture, followed by a sliced texture of same shape', - async () => { // Tests collisions in the shader cache. - // Make a 2x3 tensor, upload to gpu and reshape to 3x2. - const input = tf.tensor([[1, 2, 3], [4, 5, 6]]).abs().as2D(3, 2); - // Make a sliced version of the same tensor with the same shape. - const slicedInput = tf.slice(input, [0, 0], [3, 2]); - // First square program takes the non-sliced input. - const a = input.square(); - expectArraysClose(await a.data(), [1, 4, 9, 16, 25, 36]); - // Second square program takes the sliced input. - const b = tf.square(slicedInput); - expectArraysClose(await b.data(), [1, 4, 9, 16, 25, 36]); - }); - - it('slice a tensor and do async read', async () => { - const a = [ - [1, 2, 3, 4], - [5, 6, 7, 8], - [9, 10, 11, 12], - ]; // 3x4. - const b = tf.slice(a, [0, 1], [3, 2]); - expect(b.shape).toEqual([3, 2]); - const vals = await b.data(); - expectArraysClose(vals, new Float32Array([2, 3, 6, 7, 10, 11])); - }); - - it('flatten a sliced tensor that was continuous in memory', async () => { - const a = [ - [1, 2, 3, 4], - [5, 6, 7, 8], - [9, 10, 11, 12], - ]; // 3x4. - const b = tf.slice(a, [1, 0]).flatten(); - expect(b.shape).toEqual([8]); - expectArraysClose(await b.data(), [5, 6, 7, 8, 9, 10, 11, 12]); - }); - - it('slice a tensor that was not continuous in memory', async () => { - const a = [ - [1, 2, 3, 4], - [5, 6, 7, 8], - [9, 10, 11, 12], - ]; // 3x4. - const b = tf.slice(a, [0, 1]); - expect(b.shape).toEqual([3, 3]); - expectArraysClose(await b.data(), [2, 3, 4, 6, 7, 8, 10, 11, 12]); - }); - - it('flatten a sliced tensor that was not continuous in memory', async () => { - const a = [ - [1, 2, 3, 4], - [5, 6, 7, 8], - [9, 10, 11, 12], - ]; // 3x4. - const b = tf.slice(a, [0, 1]).flatten(); - expect(b.shape).toEqual([9]); - expectArraysClose(await b.data(), [2, 3, 4, 6, 7, 8, 10, 11, 12]); - }); - - it('flatten a sliced tensor not continuous in memory and run program', - async () => { - const a = [ - [1, 2, 3, 4], - [5, 6, 7, 8], - [9, 10, 11, 12], - ]; // 3x4. - const b = tf.slice(a, [0, 1]).flatten(); - const c = tf.square(b); - expectArraysClose(await c.data(), [4, 9, 16, 36, 49, 64, 100, 121, 144]); - }); - - it('reshape a sliced 1d into a 2d tensor', async () => { - const a = [1, 2, 3, 4, 5]; - const b = tf.slice(a, 1).as2D(2, 2); - expect(b.shape).toEqual([2, 2]); - expectArraysClose(await b.data(), [2, 3, 4, 5]); - }); - - it('reshape a sliced 1d into a 2d tensor and run program', async () => { - const a = [1, 2, 3, 4, 5]; - const b = tf.slice(a, 1).as2D(2, 2).square(); - expect(b.shape).toEqual([2, 2]); - expectArraysClose(await b.data(), [4, 9, 16, 25]); - }); - - it('broadcast the original with the sliced tensor', async () => { - const a = [[1, 2], [3, 4]]; - const b = tf.slice(a, [0, 1]); - const c = tf.add(a, b); - expect(c.shape).toEqual([2, 2]); - expectArraysClose(await c.data(), [3, 4, 7, 8]); +describeWithFlags('slice ergonomics', ALL_ENVS, () => { + it('slices 2x2x2 array into 2x1x1 no size', async () => { + const a = tf.tensor3d([1, 2, 3, 4, 5, 6, 7, 8], [2, 2, 2]); + const result = a.slice([0, 1, 1]); + expect(result.shape).toEqual([2, 1, 1]); + expectArraysClose(await result.data(), [4, 8]); }); - it('zero-sized slice out of a non-zero sized tensor', async () => { - const a = tf.zeros([4, 2]); - const res = tf.slice(a, [0, 0], [0, 2]); - expect(res.shape).toEqual([0, 2]); - expectArraysClose(await res.data(), []); + it('slices 2x2x2 array into 1x2x2 with scalar begin no size', async () => { + const a = tf.tensor3d([1, 2, 3, 4, 5, 6, 7, 8], [2, 2, 2]); + const result = a.slice(1); + expect(result.shape).toEqual([1, 2, 2]); + expectArraysClose(await result.data(), [5, 6, 7, 8]); }); - it('zero-sized slice out of a zero-sized tensor', async () => { - const a = tf.zeros([0, 4]); - const res = tf.slice(a, [0, 1], [0, 3]); - expect(res.shape).toEqual([0, 3]); - expectArraysClose(await res.data(), []); + it('slices 2x2x2 array using 2d size and 2d size', async () => { + const a = tf.tensor3d([1, 2, 3, 4, 5, 6, 7, 8], [2, 2, 2]); + const result = a.slice([0, 1]); + expect(result.shape).toEqual([2, 1, 2]); + expectArraysClose(await result.data(), [3, 4, 7, 8]); }); -}); - -describeWithFlags('slice3d', ALL_ENVS, () => { - it('slices 1x1x1 into shape 1x1x1 (effectively a copy)', async () => { - const a = tf.tensor3d([[[5]]], [1, 1, 1]); - const result = tf.slice3d(a, [0, 0, 0], [1, 1, 1]); - expect(result.shape).toEqual([1, 1, 1]); - expectArraysClose(await result.data(), [5]); + it('slices 2x2x2 array using negative size', async () => { + const a = tf.tensor3d([1, 2, 3, 4, 5, 6, 7, 8], [2, 2, 2]); + const result = a.slice([0, 1], [-1, 1]); + expect(result.shape).toEqual([2, 1, 2]); + expectArraysClose(await result.data(), [3, 4, 7, 8]); }); - it('slices 2x2x2 array into 1x2x2 starting at [1, 0, 0]', async () => { + it('slices 2x2x2 array using 1d size', async () => { const a = tf.tensor3d([1, 2, 3, 4, 5, 6, 7, 8], [2, 2, 2]); - const result = tf.slice3d(a, [1, 0, 0], [1, 2, 2]); - + const result = a.slice(0, 1); expect(result.shape).toEqual([1, 2, 2]); - expectArraysClose(await result.data(), [5, 6, 7, 8]); + expectArraysClose(await result.data(), [1, 2, 3, 4]); }); - it('slices 2x2x2 array into 2x1x1 starting at [0, 1, 1]', async () => { - const a = tf.tensor3d([1, 2, 3, 4, 5, 6, 7, 8], [2, 2, 2]); - const result = tf.slice3d(a, [0, 1, 1], [2, 1, 1]); + it('throws when passed a non-tensor', () => { + expect(() => tf.slice({} as tf.Tensor, 0, 0)) + .toThrowError(/Argument 'x' passed to 'slice' must be a Tensor/); + }); + it('accepts a tensor-like object', async () => { + const a = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]; // 2x2x2 + const result = tf.slice(a, [0, 1, 1]); expect(result.shape).toEqual([2, 1, 1]); expectArraysClose(await result.data(), [4, 8]); }); - it('accepts a tensor-like object', async () => { - const a = [[[5]]]; // 1x1x1 - const result = tf.slice3d(a, [0, 0, 0], [1, 1, 1]); + it('should match source tensor dtype', () => { + const a = tf.tensor1d([1, 2, 3, 4, 5], 'int32'); + const b = a.asType('float32'); - expect(result.shape).toEqual([1, 1, 1]); - expectArraysClose(await result.data(), [5]); + expect(tf.slice(b, 0).dtype).toEqual('float32'); }); -}); - -describeWithFlags('slice4d', ALL_ENVS, () => { - it('slices 1x1x1x1 into shape 1x1x1x1 (effectively a copy)', async () => { - const a = tf.tensor4d([[[[5]]]], [1, 1, 1, 1]); - const result = tf.slice4d(a, [0, 0, 0, 0], [1, 1, 1, 1]); - expect(result.shape).toEqual([1, 1, 1, 1]); - expectArraysClose(await result.data(), [5]); + it('throws when begin is negative', async () => { + const a = [[1, 2], [3, 4]]; // 2x2 + expect(() => tf.slice(a, [-1, 1], [ + 1, 1 + ])).toThrowError(/slice\(\) does not support negative begin indexing./); }); +}); - it('slices 2x2x2x2 array into 1x2x2x2 starting at [1, 0, 0, 0]', async () => { - const a = tf.tensor4d( - [1, 2, 3, 4, 5, 6, 7, 8, 11, 22, 33, 44, 55, 66, 77, 88], - [2, 2, 2, 2], - ); - const result = tf.slice4d(a, [1, 0, 0, 0], [1, 2, 2, 2]); - - expect(result.shape).toEqual([1, 2, 2, 2]); - expectArraysClose(await result.data(), [11, 22, 33, 44, 55, 66, 77, 88]); +describeWithFlags('shallow slicing', ALL_ENVS, () => { + it('shallow slice an input that was cast', async () => { + const a = tf.tensor([[1, 2], [3, 4]], [2, 2], 'int32'); + const b = a.toFloat(); + const c = b.slice(1, 1); + expect(c.dtype).toBe('float32'); + expect(c.shape).toEqual([1, 2]); + expectArraysClose(await c.data(), [3, 4]); }); - it('slices 2x2x2x2 array into 2x1x1x1 starting at [0, 1, 1, 1]', async () => { - const a = tf.tensor4d( - [1, 2, 3, 4, 5, 6, 7, 8, 11, 22, 33, 44, 55, 66, 77, 88], [2, 2, 2, 2]); - const result = tf.slice4d(a, [0, 1, 1, 1], [2, 1, 1, 1]); - - expect(result.shape).toEqual([2, 1, 1, 1]); - expectArraysClose(await result.data(), [8, 88]); + it('delayed async read of sliced tensor has no mem leak', async () => { + const a = tf.zeros([10]); + const b = tf.slice(a, 0, 1); + const nBefore = tf.memory().numTensors; + expect(nBefore).toBe(2); + await b.data(); + const nAfter = tf.memory().numTensors; + expect(nAfter).toBe(2); + tf.dispose([a, b]); + expect(tf.memory().numTensors).toBe(0); }); +}); - it('accepts a tensor-like object', async () => { - const a = [[[[5]]]]; // 1x1x1x1 - const result = tf.slice4d(a, [0, 0, 0, 0], [1, 1, 1, 1]); - - expect(result.shape).toEqual([1, 1, 1, 1]); - expectArraysClose(await result.data(), [5]); +describeWithFlags('shallow slicing', SYNC_BACKEND_ENVS, () => { + it('delayed sync read of sliced tensor has no mem leak', () => { + const a = tf.zeros([10]); + const b = tf.slice(a, 0, 1); + const nBefore = tf.memory().numTensors; + expect(nBefore).toBe(2); + b.dataSync(); + const nAfter = tf.memory().numTensors; + expect(nAfter).toBe(2); + tf.dispose([a, b]); + expect(tf.memory().numTensors).toBe(0); }); }); @@ -458,103 +229,3 @@ describeWithFlags('slice6d', ALL_ENVS, () => { expectArraysClose(await result.data(), [5]); }); }); - -describeWithFlags('slice ergonomics', ALL_ENVS, () => { - it('slices 2x2x2 array into 2x1x1 no size', async () => { - const a = tf.tensor3d([1, 2, 3, 4, 5, 6, 7, 8], [2, 2, 2]); - const result = a.slice([0, 1, 1]); - expect(result.shape).toEqual([2, 1, 1]); - expectArraysClose(await result.data(), [4, 8]); - }); - - it('slices 2x2x2 array into 1x2x2 with scalar begin no size', async () => { - const a = tf.tensor3d([1, 2, 3, 4, 5, 6, 7, 8], [2, 2, 2]); - const result = a.slice(1); - expect(result.shape).toEqual([1, 2, 2]); - expectArraysClose(await result.data(), [5, 6, 7, 8]); - }); - - it('slices 2x2x2 array using 2d size and 2d size', async () => { - const a = tf.tensor3d([1, 2, 3, 4, 5, 6, 7, 8], [2, 2, 2]); - const result = a.slice([0, 1]); - expect(result.shape).toEqual([2, 1, 2]); - expectArraysClose(await result.data(), [3, 4, 7, 8]); - }); - - it('slices 2x2x2 array using negative size', async () => { - const a = tf.tensor3d([1, 2, 3, 4, 5, 6, 7, 8], [2, 2, 2]); - const result = a.slice([0, 1], [-1, 1]); - expect(result.shape).toEqual([2, 1, 2]); - expectArraysClose(await result.data(), [3, 4, 7, 8]); - }); - - it('slices 2x2x2 array using 1d size', async () => { - const a = tf.tensor3d([1, 2, 3, 4, 5, 6, 7, 8], [2, 2, 2]); - const result = a.slice(0, 1); - expect(result.shape).toEqual([1, 2, 2]); - expectArraysClose(await result.data(), [1, 2, 3, 4]); - }); - - it('throws when passed a non-tensor', () => { - expect(() => tf.slice({} as tf.Tensor, 0, 0)) - .toThrowError(/Argument 'x' passed to 'slice' must be a Tensor/); - }); - - it('accepts a tensor-like object', async () => { - const a = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]; // 2x2x2 - const result = tf.slice(a, [0, 1, 1]); - expect(result.shape).toEqual([2, 1, 1]); - expectArraysClose(await result.data(), [4, 8]); - }); - - it('should match source tensor dtype', () => { - const a = tf.tensor1d([1, 2, 3, 4, 5], 'int32'); - const b = a.asType('float32'); - - expect(tf.slice(b, 0).dtype).toEqual('float32'); - }); - - it('throws when begin is negative', async () => { - const a = [[1, 2], [3, 4]]; // 2x2 - expect(() => tf.slice(a, [-1, 1], [ - 1, 1 - ])).toThrowError(/slice\(\) does not support negative begin indexing./); - }); -}); - -describeWithFlags('shallow slicing', ALL_ENVS, () => { - it('shallow slice an input that was cast', async () => { - const a = tf.tensor([[1, 2], [3, 4]], [2, 2], 'int32'); - const b = a.toFloat(); - const c = b.slice(1, 1); - expect(c.dtype).toBe('float32'); - expect(c.shape).toEqual([1, 2]); - expectArraysClose(await c.data(), [3, 4]); - }); - - it('delayed async read of sliced tensor has no mem leak', async () => { - const a = tf.zeros([10]); - const b = tf.slice(a, 0, 1); - const nBefore = tf.memory().numTensors; - expect(nBefore).toBe(2); - await b.data(); - const nAfter = tf.memory().numTensors; - expect(nAfter).toBe(2); - tf.dispose([a, b]); - expect(tf.memory().numTensors).toBe(0); - }); -}); - -describeWithFlags('shallow slicing', SYNC_BACKEND_ENVS, () => { - it('delayed sync read of sliced tensor has no mem leak', () => { - const a = tf.zeros([10]); - const b = tf.slice(a, 0, 1); - const nBefore = tf.memory().numTensors; - expect(nBefore).toBe(2); - b.dataSync(); - const nAfter = tf.memory().numTensors; - expect(nAfter).toBe(2); - tf.dispose([a, b]); - expect(tf.memory().numTensors).toBe(0); - }); -}); diff --git a/tfjs-core/src/ops/slice_util.ts b/tfjs-core/src/ops/slice_util.ts index 096e2966356..9889cbb4c26 100644 --- a/tfjs-core/src/ops/slice_util.ts +++ b/tfjs-core/src/ops/slice_util.ts @@ -268,3 +268,42 @@ export function computeFlatOffset(begin: number[], strides: number[]): number { } return flatOffset; } + +export function parseSliceParams( + x: Tensor, begin: number|number[], size?: number|number[]) { + // The following logic allows for more ergonomic calls. + let begin_: number[]; + if (typeof begin === 'number') { + begin_ = [begin, ...new Array(x.rank - 1).fill(0)]; + } else if (begin.length < x.rank) { + begin_ = begin.concat(new Array(x.rank - begin.length).fill(0)); + } else { + begin_ = begin.slice(); + } + begin_.forEach(d => { + util.assert( + d !== -1, () => 'slice() does not support negative begin indexing.'); + }); + let size_: number[]; + if (size == null) { + size_ = new Array(x.rank).fill(-1); + } else if (typeof size === 'number') { + size_ = [size, ...new Array(x.rank - 1).fill(-1)]; + } else if (size.length < x.rank) { + size_ = size.concat(new Array(x.rank - size.length).fill(-1)); + } else { + size_ = size; + } + size_ = size_.map((d, i) => { + if (d >= 0) { + return d; + } else { + util.assert( + d === -1, + () => `Negative size values should be exactly -1 but got ` + + `${d} for the slice() size at index ${i}.`); + return x.shape[i] - begin_[i]; + } + }); + return [begin_, size_]; +} diff --git a/tfjs-core/src/register_all_gradients.ts b/tfjs-core/src/register_all_gradients.ts index b6b7755a79e..989e78e2ec6 100644 --- a/tfjs-core/src/register_all_gradients.ts +++ b/tfjs-core/src/register_all_gradients.ts @@ -63,6 +63,7 @@ import {reverseGradConfig} from './gradients/Reverse_grad'; import {selectV2PoolGradConfig} from './gradients/SelectV2_grad'; import {seluGradConfig} from './gradients/Selu_grad'; import {signGradConfig} from './gradients/Sign_grad'; +import {sliceGradConfig} from './gradients/Slice_grad'; import {spaceToBatchNDGradConfig} from './gradients/SpaceToBatchND_grad'; import {splitVGradConfig} from './gradients/SplitV_grad'; import {squareGradConfig} from './gradients/Square_grad'; @@ -130,6 +131,7 @@ const gradConfigs: GradConfig[] = [ selectV2PoolGradConfig, seluGradConfig, signGradConfig, + sliceGradConfig, spaceToBatchNDGradConfig, spaceToBatchNDGradConfig, splitVGradConfig, diff --git a/tfjs-core/src/tests.ts b/tfjs-core/src/tests.ts index 7bc70093dff..3ce56135776 100644 --- a/tfjs-core/src/tests.ts +++ b/tfjs-core/src/tests.ts @@ -147,6 +147,10 @@ import './ops/selu_test'; import './ops/sigmoid_cross_entropy_test'; import './ops/sign_test'; import './ops/signal_ops_test'; +import './ops/slice1d_test'; +import './ops/slice2d_test'; +import './ops/slice3d_test'; +import './ops/slice4d_test'; import './ops/slice_test'; import './ops/slice_util_test'; import './ops/softmax_cross_entropy_test'; From f561aa9b7b896ca6e029ef61512c726dbf1d6175 Mon Sep 17 00:00:00 2001 From: Yannick Assogba Date: Wed, 8 Jul 2020 16:37:41 -0400 Subject: [PATCH 3/4] fix wasm slice --- tfjs-backend-wasm/src/kernels/Slice.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tfjs-backend-wasm/src/kernels/Slice.ts b/tfjs-backend-wasm/src/kernels/Slice.ts index e09fa7ef6db..93294b37d3f 100644 --- a/tfjs-backend-wasm/src/kernels/Slice.ts +++ b/tfjs-backend-wasm/src/kernels/Slice.ts @@ -15,7 +15,7 @@ * ============================================================================= */ -import {backend_util, buffer, NamedAttrMap, NamedTensorInfoMap, registerKernel, slice_util, util} from '@tensorflow/tfjs-core'; +import {backend_util, buffer, NamedAttrMap, NamedTensorInfoMap, registerKernel, slice_util, Tensor, util} from '@tensorflow/tfjs-core'; import {TensorInfo} from '@tensorflow/tfjs-core'; import {BackendWasm} from '../backend_wasm'; @@ -32,33 +32,34 @@ interface SliceAttrs extends NamedAttrMap { export function slice( args: {inputs: SliceInputs, attrs: SliceAttrs, backend: BackendWasm}) { const {inputs: {x}, attrs: {begin, size}, backend} = args; - const isContinous = slice_util.isSliceContinous(x.shape, begin, size); + const [begin_, size_] = slice_util.parseSliceParams(x as Tensor, begin, size); + const isContinous = slice_util.isSliceContinous(x.shape, begin_, size_); const xVals = backend.typedArrayFromHeap(x); - const out = backend.makeOutput(size, x.dtype); + const out = backend.makeOutput(size_, x.dtype); const outVals = backend.typedArrayFromHeap(out); const xStrides = util.computeStrides(x.shape); if (isContinous) { - const flatOffset = slice_util.computeFlatOffset(begin, xStrides); + const flatOffset = slice_util.computeFlatOffset(begin_, xStrides); outVals.set( - xVals.subarray(flatOffset, flatOffset + util.sizeFromShape(size))); + xVals.subarray(flatOffset, flatOffset + util.sizeFromShape(size_))); return out; } const rank = x.shape.length; if (rank === 2) { slice2d( - xVals, xStrides[0], outVals, begin as [number, number], - size as [number, number]); + xVals, xStrides[0], outVals, begin_ as [number, number], + size_ as [number, number]); } else if (rank === 3) { slice3d( xVals, xStrides[0], xStrides[1], outVals, - begin as [number, number, number], size as [number, number, number]); + begin_ as [number, number, number], size_ as [number, number, number]); } else if (rank === 4) { slice4d( xVals, xStrides[0], xStrides[1], xStrides[2], outVals, - begin as [number, number, number, number], - size as [number, number, number, number]); + begin_ as [number, number, number, number], + size_ as [number, number, number, number]); } else { - genericSliceSlow(xVals, x, outVals, begin, size); + genericSliceSlow(xVals, x, outVals, begin_, size_); } return out; } From 26f6b5f908736690315633c8c894694c036aacaf Mon Sep 17 00:00:00 2001 From: Yannick Assogba Date: Wed, 8 Jul 2020 17:21:02 -0400 Subject: [PATCH 4/4] save --- tfjs-core/src/tests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tfjs-core/src/tests.ts b/tfjs-core/src/tests.ts index 26b392b3dfc..0bdc0327a14 100644 --- a/tfjs-core/src/tests.ts +++ b/tfjs-core/src/tests.ts @@ -156,12 +156,12 @@ import './ops/selu_test'; import './ops/sigmoid_cross_entropy_test'; import './ops/sign_test'; import './ops/signal_ops_test'; +import './ops/sin_test'; +import './ops/sinh_test'; import './ops/slice1d_test'; import './ops/slice2d_test'; import './ops/slice3d_test'; import './ops/slice4d_test'; -import './ops/sin_test'; -import './ops/sinh_test'; import './ops/slice_test'; import './ops/slice_util_test'; import './ops/softmax_cross_entropy_test';