Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,29 @@ private static List<Output<?>> fromNativeOutputs(Graph g, TF_Output nativeOutput
}

/**
* Put the Java outputs into the array of native outputs, resizing it to the necessary size.
*
* @param outputs the outputs to put
* @return pointer to the native array of outputs
*/
* Put the Java outputs into the array of native outputs, resizing it to the necessary size.
*
* @param outputs the outputs to put
* @return pointer to the native array of outputs
*/
private static TF_Output toNativeOutputs(List<Operand<?>> outputs) {
// Use malloc to allocate native outputs, as they will be freed by the native layer and we do
// not want JavaCPP to deallocate them
var nativeOutputs =
new TF_Output(Pointer.malloc((long) outputs.size() * Pointer.sizeof(TF_Output.class)));

for (int i = 0; i < outputs.size(); ++i) {
var output = outputs.get(i).asOutput();
Operand<?> operand = outputs.get(i);
var nativeOutput = nativeOutputs.getPointer(i);

// Convention: null Operand => NoGradient
if (operand == null) {
nativeOutput.oper((TF_Operation) null);
nativeOutput.index(0);
continue;
}

var output = operand.asOutput();
nativeOutput.oper(((GraphOperation) output.op()).getUnsafeNativeHandle());
nativeOutput.index(output.index());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,10 @@ public static synchronized boolean registerCustomGradient(
if (hasGradient(opType)) {
return false;
}
TFJ_GradFuncAdapter g = RawCustomGradient.adapter(gradient);

org.tensorflow.op.GradientDispatch.putRaw(opType, gradient);
Comment thread
Craigacp marked this conversation as resolved.
Outdated
TFJ_GradFuncAdapter g = org.tensorflow.op.GradientDispatch.adapter();

if (!TFJ_RegisterCustomGradient(opType, g)) {
return false;
}
Expand Down Expand Up @@ -255,7 +258,10 @@ public static synchronized <T extends RawOpInputs<?>> boolean registerCustomGrad
if (hasGradient(opType)) {
return false;
}
TFJ_GradFuncAdapter g = CustomGradient.adapter(gradient, inputClass);

org.tensorflow.op.GradientDispatch.putTyped(opType, gradient, inputClass);
TFJ_GradFuncAdapter g = org.tensorflow.op.GradientDispatch.adapter();

if (!TFJ_RegisterCustomGradient(opType, g)) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@
package org.tensorflow.op;

import java.util.List;
import org.bytedeco.javacpp.PointerPointer;
import org.tensorflow.Operand;
import org.tensorflow.Output;
import org.tensorflow.TensorFlow;
import org.tensorflow.internal.c_api.TFJ_GradFuncAdapter;
import org.tensorflow.internal.c_api.TFJ_GraphId;
import org.tensorflow.internal.c_api.TFJ_Scope;
import org.tensorflow.internal.c_api.TF_Operation;
import org.tensorflow.internal.c_api.TF_Output;

/**
* A custom gradient for ops of type {@link T}. Should be registered using {@link
Expand Down Expand Up @@ -57,6 +62,31 @@ public interface CustomGradient<T extends RawOpInputs> {
*/
static <T extends RawOpInputs<?>> TFJ_GradFuncAdapter adapter(
CustomGradient<T> gradient, Class<T> opClass) {
return new TypedGradientAdapter<T>(gradient, opClass);

final TypedGradientAdapter<T> impl = new TypedGradientAdapter<T>(gradient, opClass);

// IMPORTANT:
// Return a *direct* TFJ_GradFuncAdapter subclass, so JavaCPP reliably materializes a function
// pointer thunk for the native side. Some call paths may pass NULL if we return a deeper
// subclass.
return new TFJ_GradFuncAdapter() {
@Override
public int call(
TFJ_GraphId nativeGraphId,
TFJ_Scope nativeScope,
TF_Operation nativeOperation,
TF_Output nativeGradInputs,
int nativeGradInputsLength,
PointerPointer nativeGradOutputsPtr) {

return impl.call(
nativeGraphId,
nativeScope,
nativeOperation,
nativeGradInputs,
nativeGradInputsLength,
nativeGradOutputsPtr);
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package org.tensorflow.op;
Comment thread
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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
Expand Up @@ -17,11 +17,16 @@
package org.tensorflow.op;

import java.util.List;
import org.bytedeco.javacpp.PointerPointer;
import org.tensorflow.GraphOperation;
import org.tensorflow.Operand;
import org.tensorflow.Output;
import org.tensorflow.TensorFlow;
import org.tensorflow.internal.c_api.TFJ_GradFuncAdapter;
import org.tensorflow.internal.c_api.TFJ_GraphId;
import org.tensorflow.internal.c_api.TFJ_Scope;
import org.tensorflow.internal.c_api.TF_Operation;
import org.tensorflow.internal.c_api.TF_Output;

/**
* A custom gradient for an op of unspecified type. Should be registered using {@link
Expand Down Expand Up @@ -54,6 +59,30 @@ public interface RawCustomGradient {
* TensorFlow#registerCustomGradient(String, RawCustomGradient)}.
*/
static TFJ_GradFuncAdapter adapter(RawCustomGradient gradient) {
return new RawGradientAdapter(gradient);
final RawGradientAdapter impl = new RawGradientAdapter(gradient);

// IMPORTANT:
// Return a *direct* TFJ_GradFuncAdapter subclass, so JavaCPP reliably materializes a function
// pointer thunk for the native side. Some call paths may pass NULL if we return a deeper
// subclass.
return new TFJ_GradFuncAdapter() {
@Override
public int call(
TFJ_GraphId nativeGraphId,
TFJ_Scope nativeScope,
TF_Operation nativeOperation,
TF_Output nativeGradInputs,
int nativeGradInputsLength,
PointerPointer nativeGradOutputsPtr) {

return impl.call(
nativeGraphId,
nativeScope,
nativeOperation,
nativeGradInputs,
nativeGradInputsLength,
nativeGradOutputsPtr);
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package org.tensorflow;
Comment thread
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.");
}
}
}
Loading
Loading