Skip to content
This repository was archived by the owner on Aug 15, 2019. It is now read-only.

Commit 2db9024

Browse files
authored
Fix 1) tanh underflow error, 2) scoping bug and add 3) debug mode for math (#34)
* Fix overflow tanh overflow error, scope bug and add debug mode for math * simplify debug mode api * fix typo in trig_gpu_test * Merge master into logical * Merge master into logical * add unit tests for math debug mode * Merge branch 'logical' of https://github.com/PAIR-code/deeplearnjs into logical
1 parent a733567 commit 2db9024

File tree

4 files changed

+120
-9
lines changed

4 files changed

+120
-9
lines changed

src/math/math.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ export abstract class NDArrayMath {
2828
private ndarraysToKeep: NDArray[][] = [];
2929
private activeScopeNDArraysToKeep: NDArray[] = [];
3030

31+
private debugMode = false;
32+
3133
/**
3234
* @param safeMode In safe mode, you must use math operations inside
33-
* a math.scope() which will automatically clean up intermediate NDArrays.
35+
* a math.scope() which will automatically clean up intermediate NDArrays.
3436
*/
3537
constructor(private safeMode: boolean) {}
3638

@@ -57,6 +59,18 @@ export abstract class NDArrayMath {
5759
return result;
5860
}
5961

62+
63+
/**
64+
* In debug mode, the output of every math call will be downloaded to the CPU
65+
* and checked for NaNs. This significantly impacts performance.
66+
*/
67+
enableDebugMode() {
68+
this.debugMode = true;
69+
console.warn('Debugging mode is ON. The output of every math call will ' +
70+
'be downloaded to CPU and checked for NaNs. ' +
71+
'This significantly impacts performance.');
72+
}
73+
6074
/**
6175
* Start a scope. Use this with endScope() to achieve the same functionality
6276
* as scope() without the need for a function closure.
@@ -76,13 +90,14 @@ export abstract class NDArrayMath {
7690
* as scope() without the need for a function closure.
7791
*/
7892
endScope(result: ScopeResult) {
93+
let arraysToKeep = this.activeScopeNDArraysToKeep;
94+
if (result != null) {
95+
arraysToKeep = arraysToKeep.concat(result as NDArray|NDArray[]);
96+
}
7997
// Dispose the current scope.
8098
for (let i = 0; i < this.activeScope.length; i++) {
8199
const ndarray = this.activeScope[i];
82-
83-
if (this.isNDArrayDataInList(ndarray, this.activeScopeNDArraysToKeep) ||
84-
(result != null && result instanceof NDArray &&
85-
ndarray.getData() === (result as NDArray).getData())) {
100+
if (this.isNDArrayDataInList(ndarray, arraysToKeep)) {
86101
continue;
87102
}
88103
ndarray.dispose();
@@ -141,12 +156,24 @@ export abstract class NDArrayMath {
141156
return result;
142157
}
143158

159+
private checkForNaN(arr: NDArray): void {
160+
const vals = arr.getValues();
161+
for (let i = 0; i < vals.length; i++) {
162+
if (isNaN(vals[i])) {
163+
throw Error('The result NDArray of the last math call has NaNs.');
164+
}
165+
}
166+
}
167+
144168
/**
145169
* Tracks an NDArray in the current scope to be automatically cleaned up when
146170
* the current scope ends, and returns the value.
147171
* @param result The NDArray to track in the current scope.
148172
*/
149173
track<T extends NDArray>(result: T): T {
174+
if (this.debugMode) {
175+
this.checkForNaN(result);
176+
}
150177
if (this.activeScope == null) {
151178
if (this.safeMode) {
152179
throw new Error(

src/math/math_gpu_test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('NDArrayMathGPU scope', () => {
2727
math = new NDArrayMathGPU();
2828
});
2929

30-
it('basic scope usage with a return', () => {
30+
it('scope returns NDArray', () => {
3131
const a = Array1D.new([1, 2, 3]);
3232
let b = Array1D.new([0, 0, 0]);
3333

@@ -55,6 +55,33 @@ describe('NDArrayMathGPU scope', () => {
5555
b.dispose();
5656
});
5757

58+
it('scope returns NDArray[]', () => {
59+
const a = Array1D.new([1, 2, 3]);
60+
const b = Array1D.new([0, -1, 1]);
61+
62+
const numUsedTexturesBefore = math.getTextureManager().getNumUsedTextures();
63+
64+
math.scope(() => {
65+
const result = math.scope(() => {
66+
math.add(a, b);
67+
return [math.add(a, b), math.sub(a, b)];
68+
});
69+
70+
// a, b, and 2 results are new textures. All intermediates should be
71+
// disposed.
72+
expect(math.getTextureManager().getNumUsedTextures())
73+
.toEqual(numUsedTexturesBefore + 4);
74+
expect(result[0].getValues()).toEqual(new Float32Array([1, 1, 4]));
75+
expect(result[1].getValues()).toEqual(new Float32Array([1, 3, 2]));
76+
});
77+
78+
// a, b are new textures, result should be disposed.
79+
expect(math.getTextureManager().getNumUsedTextures())
80+
.toEqual(numUsedTexturesBefore + 2);
81+
a.dispose();
82+
b.dispose();
83+
});
84+
5885
it('basic scope usage without return', () => {
5986
const a = Array1D.new([1, 2, 3]);
6087
let b = Array1D.new([0, 0, 0]);
@@ -2128,3 +2155,36 @@ describe('NDArrayMathGPU batchNorm', () => {
21282155
offset.dispose();
21292156
});
21302157
});
2158+
2159+
describe('NDArrayMathGPU debug mode', () => {
2160+
let math: NDArrayMathGPU;
2161+
2162+
beforeEach(() => {
2163+
math = new NDArrayMathGPU();
2164+
math.startScope();
2165+
});
2166+
2167+
afterEach(() => {
2168+
math.endScope(null!);
2169+
});
2170+
2171+
it('debug mode does not error when no nans', () => {
2172+
math.enableDebugMode();
2173+
const a = Array1D.new([2, -1, 0, 3]);
2174+
const res = math.relu(a);
2175+
expect(res.getValues()).toEqual(new Float32Array([2, 0, 0, 3]));
2176+
});
2177+
2178+
it('debug mode errors when there are nans', () => {
2179+
math.enableDebugMode();
2180+
const a = Array1D.new([2, NaN]);
2181+
const f = () => math.relu(a);
2182+
expect(f).toThrowError();
2183+
});
2184+
2185+
it('no errors where there are nans, and debug mode is disabled', () => {
2186+
const a = Array1D.new([2, NaN]);
2187+
const res = math.relu(a);
2188+
expect(res.getValues()).toEqual(new Float32Array([2, NaN]));
2189+
});
2190+
});

src/math/webgl/trig_gpu_test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import * as test_util from '../../test_util';
1717
import * as util from '../../util';
1818
import {UnaryOp} from './unaryop_gpu';
1919
import * as unaryop_gpu_test from './unaryop_gpu_test';
20-
import {Array1D, Array2D, Array3D} from '../ndarray';
20+
import {Scalar, Array1D, Array2D, Array3D} from '../ndarray';
2121

2222
describe('sin_gpu', () => {
2323
it('returns a matrix with the same shape as the input matrix', () => {
@@ -58,4 +58,28 @@ describe('tanh_gpu', () => {
5858
const result = unaryop_gpu_test.uploadUnaryDownload(aArr, UnaryOp.TANH);
5959
test_util.expectArraysClose(result, expectedResult, 1e-6);
6060
});
61+
62+
it('overflow', () => {
63+
const a = Scalar.new(100);
64+
const r = unaryop_gpu_test.uploadUnaryDownload(a, UnaryOp.TANH);
65+
expect(r).toBeCloseTo(1);
66+
});
67+
68+
it('tanh(0) = 0', () => {
69+
const a = Scalar.new(0);
70+
const r = unaryop_gpu_test.uploadUnaryDownload(a, UnaryOp.TANH);
71+
expect(r).toBeCloseTo(0);
72+
});
73+
74+
it('tanh(0.01) is close to 0.01', () => {
75+
const a = Scalar.new(0.01);
76+
const r = unaryop_gpu_test.uploadUnaryDownload(a, UnaryOp.TANH);
77+
expect(r).toBeCloseTo(0.01);
78+
});
79+
80+
it('underflow', () => {
81+
const a = Scalar.new(-100);
82+
const r = unaryop_gpu_test.uploadUnaryDownload(a, UnaryOp.TANH);
83+
expect(r).toBeCloseTo(-1);
84+
});
6185
});

src/math/webgl/unaryop_gpu.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ function getOpSnippet(op: UnaryOp) {
5555
case UnaryOp.SIN:
5656
return 'float r = sin(v);';
5757
case UnaryOp.TANH:
58-
return `float e2x = exp(-2.0 * v);
59-
float r = (1.0 - e2x) / (1.0 + e2x);`;
58+
return `float e2x = exp(-2.0 * abs(v));
59+
float r = sign(v) * (1.0 - e2x) / (1.0 + e2x);`;
6060
default:
6161
throw Error('Unrecognized unary op type ' + op);
6262
}

0 commit comments

Comments
 (0)