-
Notifications
You must be signed in to change notification settings - Fork 225
Custom/gradients dispatch #632
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
bcf9e2e
7c7dc54
e2fa04b
1d43d4c
69a9a36
8d80312
1240293
fd1cc19
cdd943c
56539b9
6be7c17
da6c216
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| package org.tensorflow.op; | ||
|
Craigacp marked this conversation as resolved.
|
||
|
|
||
| import java.lang.reflect.Constructor; | ||
| import java.util.List; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.ConcurrentMap; | ||
| import org.tensorflow.AbstractGradientAdapter; | ||
| import org.tensorflow.Graph; | ||
| import org.tensorflow.GraphOperation; | ||
| import org.tensorflow.Operand; | ||
| import org.tensorflow.Output; | ||
| import org.tensorflow.internal.c_api.TFJ_Scope; | ||
|
|
||
| final class DispatchingGradientAdapter extends AbstractGradientAdapter { | ||
|
|
||
| private final ConcurrentMap<String, RawCustomGradient> raw = new ConcurrentHashMap<>(); | ||
| private final ConcurrentMap<String, TypedEntry<?>> typed = new ConcurrentHashMap<>(); | ||
|
|
||
| static final class TypedEntry<T extends RawOpInputs<?>> { | ||
| final CustomGradient<T> grad; | ||
| final Class<T> inputClass; | ||
| final Constructor<T> ctor; | ||
|
|
||
| TypedEntry(CustomGradient<T> grad, Class<T> inputClass) { | ||
| this.grad = grad; | ||
| this.inputClass = inputClass; | ||
| try { | ||
| this.ctor = inputClass.getConstructor(org.tensorflow.GraphOperation.class); | ||
| } catch (NoSuchMethodException e) { | ||
| throw new IllegalArgumentException( | ||
| "Inputs class " + inputClass.getName() + " must have a public ctor(GraphOperation).", | ||
| e); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| void putRaw(String opType, RawCustomGradient g) { | ||
| raw.put(opType, g); | ||
| } | ||
|
|
||
| <T extends RawOpInputs<?>> void putTyped( | ||
| String opType, CustomGradient<T> g, Class<T> inputClass) { | ||
| typed.put(opType, new TypedEntry<>(g, inputClass)); | ||
| } | ||
|
|
||
| @Override | ||
| protected List<Operand<?>> apply( | ||
| Graph graph, TFJ_Scope scope, GraphOperation operation, List<Output<?>> gradInputs) { | ||
|
|
||
| final String opType = operation.type(); | ||
|
|
||
| RawCustomGradient rg = raw.get(opType); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic prefers raw gradients over typed ones, but there isn't anything documented about why it prefers them or if it makes sense to add both raw and typed gradients for the same op. It would be good to clarify this, and if it doesn't make sense to have both kinds of gradients the adapter should reject them in the puts.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add javadoc to the top of this class noting the overall purpose of it (to provide Java side dispatching for gradients mirroring TF-Python), that it only accepts either raw or typed gradients for a given op, and that it rejects duplicate assignments. |
||
| if (rg != null) { | ||
| // NativeScope & Ops constructors are package-private => must be in org.tensorflow.op | ||
| Scope nativeScope = | ||
| new NativeScope(scope, graph, operation.name()).withSubScope(operation.name()); | ||
| return rg.call(new Ops(nativeScope), operation, gradInputs); | ||
| } | ||
|
|
||
| @SuppressWarnings("rawtypes") | ||
| TypedEntry te = typed.get(opType); | ||
| if (te != null) { | ||
| return applyTyped(graph, scope, operation, gradInputs, te); | ||
| } | ||
|
|
||
| throw new IllegalStateException("No Java custom gradient registered for op type: " + opType); | ||
| } | ||
|
|
||
| private <T extends RawOpInputs<?>> List<Operand<?>> applyTyped( | ||
| Graph graph, | ||
| TFJ_Scope scope, | ||
| GraphOperation operation, | ||
| List<Output<?>> gradInputs, | ||
| TypedEntry<T> te) { | ||
| try { | ||
| T inputs = te.ctor.newInstance(operation); | ||
| Scope nativeScope = | ||
| new NativeScope(scope, graph, operation.name()).withSubScope(operation.name()); | ||
| return te.grad.call(new Ops(nativeScope), inputs, gradInputs); | ||
| } catch (ReflectiveOperationException e) { | ||
| throw new RuntimeException("Failed to instantiate inputs for " + te.inputClass.getName(), e); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package org.tensorflow.op; | ||
|
|
||
| import org.tensorflow.internal.c_api.TFJ_GradFuncAdapter; | ||
|
|
||
| /** Public bridge to a single native gradient adapter. */ | ||
| public final class GradientDispatch { | ||
|
|
||
| // package-private adapter that can access NativeScope/Ops constructors | ||
| static final DispatchingGradientAdapter ADAPTER = new DispatchingGradientAdapter(); | ||
|
|
||
| private GradientDispatch() {} | ||
|
|
||
| public static TFJ_GradFuncAdapter adapter() { | ||
| return ADAPTER; | ||
| } | ||
|
|
||
| public static void putRaw(String opType, RawCustomGradient gradient) { | ||
| ADAPTER.putRaw(opType, gradient); | ||
| } | ||
|
|
||
| public static <T extends RawOpInputs<?>> void putTyped( | ||
| String opType, CustomGradient<T> gradient, Class<T> inputClass) { | ||
| ADAPTER.putTyped(opType, gradient, inputClass); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| package org.tensorflow; | ||
|
Craigacp marked this conversation as resolved.
|
||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||
|
|
||
| import java.util.List; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.api.condition.DisabledOnOs; | ||
| import org.junit.jupiter.api.condition.OS; | ||
| import org.tensorflow.op.CustomGradient; | ||
| import org.tensorflow.op.Ops; | ||
| import org.tensorflow.op.RawCustomGradient; | ||
| import org.tensorflow.op.nn.SparseSoftmaxCrossEntropyWithLogits; | ||
| import org.tensorflow.types.TFloat32; | ||
| import org.tensorflow.types.TInt32; | ||
|
|
||
| @DisabledOnOs(OS.WINDOWS) | ||
| public class CustomGradientsTest { | ||
|
|
||
| @Test | ||
| public void noGradientNullIsSupported() { | ||
| // Register a custom gradient for an op that has NO native gradient in TF core. | ||
| CustomGradient<SparseSoftmaxCrossEntropyWithLogits.Inputs> grad = | ||
| (tf, op, gradInputs) -> { | ||
| @SuppressWarnings("unchecked") | ||
| Operand<TFloat32> gLoss = (Operand<TFloat32>) gradInputs.get(0); // [B] | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| Operand<TFloat32> logits = op.features; | ||
|
|
||
| SparseSoftmaxCrossEntropyWithLogits<TFloat32> xent = | ||
| SparseSoftmaxCrossEntropyWithLogits.create(tf.scope(), logits, op.labels); | ||
|
|
||
| Operand<TFloat32> backprop = xent.backprop(); // [B,C] | ||
| Operand<TFloat32> gLossE = tf.expandDims(gLoss, tf.constant(1)); // [B,1] | ||
| Operand<TFloat32> dLogits = tf.math.mul(gLossE, backprop); // [B,C] | ||
|
|
||
| // labels: NoGradient | ||
| return java.util.Arrays.asList(dLogits, null); | ||
| }; | ||
|
|
||
| assertTrue( | ||
| TensorFlow.registerCustomGradient(SparseSoftmaxCrossEntropyWithLogits.Inputs.class, grad)); | ||
|
|
||
| try (Graph g = new Graph()) { | ||
| Ops tf = Ops.create(g); | ||
|
|
||
| // Small fixed shapes to be able to create an explicit seed (avoid OnesLike in addGradients). | ||
| Operand<TFloat32> logits = tf.constant(new float[][] {{1f, 2f, 3f}, {3f, 2f, 1f}}); | ||
| Operand<TInt32> labels = tf.constant(new int[] {2, 0}); | ||
|
|
||
| SparseSoftmaxCrossEntropyWithLogits<TFloat32> xent = | ||
| SparseSoftmaxCrossEntropyWithLogits.create(tf.scope(), logits, labels); | ||
|
|
||
| Output<TFloat32> loss = xent.loss(); // [2] | ||
| Operand<TFloat32> seed = tf.constant(new float[] {1f, 1f}); // same shape as loss | ||
|
|
||
| Output<?>[] grads = | ||
| g.addGradients( | ||
| "seed", | ||
| new Output<?>[] {loss}, | ||
| new Output<?>[] {logits.asOutput(), labels.asOutput()}, | ||
| new Output<?>[] {seed.asOutput()}); | ||
|
|
||
| // logits grad exists, labels grad must be "NoGradient" (represented as a CLOSED Output) | ||
| assertNotNull(grads); | ||
| assertEquals(2, grads.length); | ||
| assertNotNull(grads[0], "Expected gradient for logits"); | ||
| assertNotNull(grads[1], "Expected an Output placeholder for labels gradient"); | ||
| assertTrue(grads[1].isClosed(), "Expected closed gradient (NoGradient) for labels"); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void sigmoidGradHasCustomGradientWithoutOnesLikeSeed() { | ||
| // Register custom gradient for SigmoidGrad (if already registered, it will return false, | ||
| // but the test can still pass because the gradient exists in the current process). | ||
| TensorFlow.registerCustomGradient( | ||
| "SigmoidGrad", | ||
| (RawCustomGradient) | ||
| (tf, op, gradInputs) -> { | ||
| @SuppressWarnings("unchecked") | ||
| Operand<TFloat32> y = (Operand<TFloat32>) op.input(0); // sigmoid(x) | ||
| @SuppressWarnings("unchecked") | ||
| Operand<TFloat32> dy = (Operand<TFloat32>) op.input(1); // upstream into SigmoidGrad | ||
| @SuppressWarnings("unchecked") | ||
| Operand<TFloat32> upstream = (Operand<TFloat32>) gradInputs.get(0); | ||
|
|
||
| Operand<TFloat32> one = tf.constant(1.0f); | ||
| Operand<TFloat32> yTimesOneMinusY = tf.math.mul(y, tf.math.sub(one, y)); | ||
|
|
||
| // dL/d(dy) = upstream * y*(1-y) | ||
| Operand<TFloat32> dDy = tf.math.mul(upstream, yTimesOneMinusY); | ||
|
|
||
| // dL/d(y) not needed for this test; return zeros to keep it non-null. | ||
| Operand<TFloat32> dY = tf.zerosLike(y); | ||
|
|
||
| return java.util.Arrays.asList(dY, dDy); | ||
| }); | ||
|
|
||
| try (Graph g = new Graph()) { | ||
| Ops tf = Ops.create(g); | ||
|
|
||
| Operand<TFloat32> x = tf.placeholder(TFloat32.class); | ||
| Operand<TFloat32> y = tf.math.sigmoid(x); | ||
|
|
||
| // Provide an explicit seed dy to avoid Graph.addGradients defaulting to OnesLike(y) | ||
| Operand<TFloat32> seed = tf.fill(tf.shape(y), tf.constant(1.0f)); | ||
|
|
||
| Output<?>[] grads = | ||
| g.addGradients( | ||
| "seed", | ||
| new Output<?>[] {y.asOutput()}, | ||
| new Output<?>[] {x.asOutput()}, | ||
| new Output<?>[] {seed.asOutput()}); | ||
|
|
||
| assertNotNull(grads); | ||
| assertEquals(1, grads.length); | ||
| assertNotNull(grads[0], "Expected a non-null gradient for sigmoid(x) wrt x."); | ||
| assertFalse(grads[0].isClosed(), "Expected an active Output for d(sigmoid)/dx."); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.