Nanosecond latency measurement for OVIC.

PiperOrigin-RevId: 308144385
Change-Id: I19c5942789110ad1ab92130c0d9914e78dfed1c0
This commit is contained in:
Alex Stark 2020-04-23 15:40:57 -07:00 committed by TensorFlower Gardener
parent d2a5485e83
commit 9533f3f841
9 changed files with 105 additions and 58 deletions

View File

@ -45,6 +45,7 @@ public class OvicBenchmarkerActivity extends Activity {
/** Name of the task-dependent data files stored in Assets. */ /** Name of the task-dependent data files stored in Assets. */
private static String labelPath = null; private static String labelPath = null;
private static String testImagePath = null; private static String testImagePath = null;
private static String modelPath = null; private static String modelPath = null;
/** /**
@ -91,7 +92,7 @@ public class OvicBenchmarkerActivity extends Activity {
labelPath = "labels.txt"; labelPath = "labels.txt";
testImagePath = "test_image_224.jpg"; testImagePath = "test_image_224.jpg";
modelPath = "quantized_model.lite"; modelPath = "quantized_model.lite";
} else { // Benchmarking detection. } else { // Benchmarking detection.
benchmarker = new OvicDetectorBenchmarker(WALL_TIME); benchmarker = new OvicDetectorBenchmarker(WALL_TIME);
labelPath = "coco_labels.txt"; labelPath = "coco_labels.txt";
testImagePath = "test_image_224.jpg"; testImagePath = "test_image_224.jpg";
@ -145,6 +146,7 @@ public class OvicBenchmarkerActivity extends Activity {
public void detectPressed(View view) throws IOException { public void detectPressed(View view) throws IOException {
benchmarkSession(false); benchmarkSession(false);
} }
public void classifyPressed(View view) throws IOException { public void classifyPressed(View view) throws IOException {
benchmarkSession(true); benchmarkSession(true);
} }
@ -194,7 +196,7 @@ public class OvicBenchmarkerActivity extends Activity {
displayText displayText
+ modelPath + modelPath
+ ": Average latency=" + ": Average latency="
+ df2.format(benchmarker.getTotalRunTime() / testIter) + df2.format(benchmarker.getTotalRuntimeNano() * 1.0e-6 / testIter)
+ "ms after " + "ms after "
+ testIter + testIter
+ " runs."); + " runs.");
@ -204,12 +206,15 @@ public class OvicBenchmarkerActivity extends Activity {
} }
} }
// TODO(b/153429929) Remove with resolution of issue (see below).
@SuppressWarnings("RuntimeExec")
private static void setProcessorAffinity(int mask) throws IOException { private static void setProcessorAffinity(int mask) throws IOException {
int myPid = Process.myPid(); int myPid = Process.myPid();
Log.i(TAG, String.format("Setting processor affinity to 0x%02x", mask)); Log.i(TAG, String.format("Setting processor affinity to 0x%02x", mask));
String command = String.format("taskset -a -p %x %d", mask, myPid); String command = String.format("taskset -a -p %x %d", mask, myPid);
try { try {
// TODO(b/153429929) This is deprecated, but updating is not safe while verification is hard.
Runtime.getRuntime().exec(command).waitFor(); Runtime.getRuntime().exec(command).waitFor();
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw new IOException("Interrupted: " + e); throw new IOException("Interrupted: " + e);

View File

@ -43,6 +43,7 @@ public abstract class OvicBenchmarker {
/** Dimensions of inputs. */ /** Dimensions of inputs. */
protected static final int DIM_BATCH_SIZE = 1; protected static final int DIM_BATCH_SIZE = 1;
protected static final int DIM_PIXEL_SIZE = 3; protected static final int DIM_PIXEL_SIZE = 3;
protected int imgHeight = 224; protected int imgHeight = 224;
protected int imgWidth = 224; protected int imgWidth = 224;
@ -53,38 +54,38 @@ public abstract class OvicBenchmarker {
/** A ByteBuffer to hold image data, to be feed into classifier as inputs. */ /** A ByteBuffer to hold image data, to be feed into classifier as inputs. */
protected ByteBuffer imgData = null; protected ByteBuffer imgData = null;
/** Total runtime in ms. */ /** Total runtime in ns. */
protected double totalRuntime = 0.0; protected double totalRuntimeNano = 0.0;
/** Total allowed runtime in ms. */ /** Total allowed runtime in ms. */
protected double wallTime = 20000 * 30.0; protected double wallTimeNano = 20000 * 30 * 1.0e6;
/** Record whether benchmark has started (used to skip the first image). */ /** Record whether benchmark has started (used to skip the first image). */
protected boolean benchmarkStarted = false; protected boolean benchmarkStarted = false;
/** /**
* Initializes an {@link OvicBenchmarker} * Initializes an {@link OvicBenchmarker}
* *
* @param wallTime: a double number specifying the total amount of time to benchmark. * @param wallTimeNano: a double number specifying the total amount of time to benchmark.
*/ */
public OvicBenchmarker(double wallTime) { public OvicBenchmarker(double wallTimeNano) {
benchmarkStarted = false; benchmarkStarted = false;
totalRuntime = 0.0; totalRuntimeNano = 0.0;
this.wallTime = wallTime; this.wallTimeNano = wallTimeNano;
} }
/** Return the cumulative latency of all runs so far. */ /** Return the cumulative latency of all runs so far. */
public double getTotalRunTime() { public double getTotalRuntimeNano() {
return totalRuntime; return totalRuntimeNano;
} }
/** Check whether the benchmarker should stop. */ /** Check whether the benchmarker should stop. */
public Boolean shouldStop() { public Boolean shouldStop() {
if (totalRuntime >= wallTime) { if (totalRuntimeNano >= wallTimeNano) {
Log.e( Log.e(
TAG, TAG,
"Total runtime " "Total runtime (ms) "
+ Double.toString(totalRuntime) + (totalRuntimeNano * 1.0e-6)
+ " exceeded walltime " + " exceeded wall-time "
+ Double.toString(wallTime)); + (wallTimeNano * 1.0e-6));
return true; return true;
} }
return false; return false;
@ -120,9 +121,9 @@ public abstract class OvicBenchmarker {
public abstract String getLastResultString(); public abstract String getLastResultString();
/** /**
* Loads input buffer from intValues into ByteBuffer for the interpreter. * Loads input buffer from intValues into ByteBuffer for the interpreter. Input buffer must be
* Input buffer must be loaded in intValues and output will be placed in imgData. * loaded in intValues and output will be placed in imgData.
*/ */
protected void loadsInputToByteBuffer() { protected void loadsInputToByteBuffer() {
if (imgData == null || intValues == null) { if (imgData == null || intValues == null) {
throw new RuntimeException("Benchmarker is not yet ready to test."); throw new RuntimeException("Benchmarker is not yet ready to test.");

View File

@ -21,34 +21,39 @@ public class OvicClassificationResult {
/** Top K classes and probabilities. */ /** Top K classes and probabilities. */
public final ArrayList<String> topKClasses; public final ArrayList<String> topKClasses;
public final ArrayList<Float> topKProbs; public final ArrayList<Float> topKProbs;
public final ArrayList<Integer> topKIndices; public final ArrayList<Integer> topKIndices;
/** Latency (ms). */ /** Latency (ms). */
public Long latency; public Long latencyMilli;
/** Latency (ns). */
public Long latencyNano;
OvicClassificationResult() { OvicClassificationResult() {
topKClasses = new ArrayList<>(); topKClasses = new ArrayList<>();
topKProbs = new ArrayList<>(); topKProbs = new ArrayList<>();
topKIndices = new ArrayList<>(); topKIndices = new ArrayList<>();
latency = -1L; latencyMilli = -1L;
latencyNano = -1L;
} }
@Override @Override
public String toString() { public String toString() {
String textToShow = latency + "ms"; String textToShow = latencyMilli + "ms";
textToShow += "\n" + latencyNano + "ns";
for (int k = 0; k < topKProbs.size(); ++k) { for (int k = 0; k < topKProbs.size(); ++k) {
textToShow += textToShow +=
"\nPrediction [" "\nPrediction ["
+ k + k
+ "] = Class " + "] = Class "
+ Integer.toString(topKIndices.get(k)) + topKIndices.get(k)
+ " (" + " ("
+ topKClasses.get(k) + topKClasses.get(k)
+ ") : " + ") : "
+ Float.toString(topKProbs.get(k)); + topKProbs.get(k);
} }
return textToShow; return textToShow;
} }
} }

View File

@ -14,13 +14,14 @@ limitations under the License.
==============================================================================*/ ==============================================================================*/
package org.tensorflow.ovic; package org.tensorflow.ovic;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer; import java.nio.MappedByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.AbstractMap; import java.util.AbstractMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -44,7 +45,7 @@ public class OvicClassifier {
private Interpreter tflite; private Interpreter tflite;
/** Labels corresponding to the output of the vision model. */ /** Labels corresponding to the output of the vision model. */
private List<String> labelList; private final List<String> labelList;
/** An array to hold inference results, to be feed into Tensorflow Lite as outputs. */ /** An array to hold inference results, to be feed into Tensorflow Lite as outputs. */
private byte[][] inferenceOutputArray = null; private byte[][] inferenceOutputArray = null;
@ -56,19 +57,18 @@ public class OvicClassifier {
/** Whether the model runs as float or quantized. */ /** Whether the model runs as float or quantized. */
private Boolean outputIsFloat = null; private Boolean outputIsFloat = null;
private PriorityQueue<Map.Entry<Integer, Float>> sortedLabels = private final PriorityQueue<Map.Entry<Integer, Float>> sortedLabels =
new PriorityQueue<>( new PriorityQueue<>(
RESULTS_TO_SHOW, RESULTS_TO_SHOW,
new Comparator<Map.Entry<Integer, Float>>() { new Comparator<Map.Entry<Integer, Float>>() {
@Override @Override
public int compare(Map.Entry<Integer, Float> o1, Map.Entry<Integer, Float> o2) { public int compare(Map.Entry<Integer, Float> o1, Map.Entry<Integer, Float> o2) {
return (o1.getValue()).compareTo(o2.getValue()); return o1.getValue().compareTo(o2.getValue());
} }
}); });
/** Initializes an {@code OvicClassifier}. */ /** Initializes an {@code OvicClassifier}. */
public OvicClassifier(InputStream labelInputStream, MappedByteBuffer model) public OvicClassifier(InputStream labelInputStream, MappedByteBuffer model) throws IOException {
throws IOException, RuntimeException {
if (model == null) { if (model == null) {
throw new RuntimeException("Input model is empty."); throw new RuntimeException("Input model is empty.");
} }
@ -80,12 +80,12 @@ public class OvicClassifier {
throw new RuntimeException("The model's input dimensions must be 4 (BWHC)."); throw new RuntimeException("The model's input dimensions must be 4 (BWHC).");
} }
if (inputDims[0] != 1) { if (inputDims[0] != 1) {
throw new RuntimeException("The model must have a batch size of 1, got " throw new IllegalStateException(
+ inputDims[0] + " instead."); "The model must have a batch size of 1, got " + inputDims[0] + " instead.");
} }
if (inputDims[3] != 3) { if (inputDims[3] != 3) {
throw new RuntimeException("The model must have three color channels, got " throw new IllegalStateException(
+ inputDims[3] + " instead."); "The model must have three color channels, got " + inputDims[3] + " instead.");
} }
int minSide = Math.min(inputDims[1], inputDims[2]); int minSide = Math.min(inputDims[1], inputDims[2]);
int maxSide = Math.max(inputDims[1], inputDims[2]); int maxSide = Math.max(inputDims[1], inputDims[2]);
@ -93,12 +93,15 @@ public class OvicClassifier {
throw new RuntimeException("The model's resolution must be between (0, 1000]."); throw new RuntimeException("The model's resolution must be between (0, 1000].");
} }
String outputDataType = TestHelper.getOutputDataType(tflite, 0); String outputDataType = TestHelper.getOutputDataType(tflite, 0);
if (outputDataType.equals("float")) { switch (outputDataType) {
outputIsFloat = true; case "float":
} else if (outputDataType.equals("byte")) { outputIsFloat = true;
outputIsFloat = false; break;
} else { case "byte":
throw new RuntimeException("Cannot process output type: " + outputDataType); outputIsFloat = false;
break;
default:
throw new IllegalStateException("Cannot process output type: " + outputDataType);
} }
inferenceOutputArray = new byte[1][labelList.size()]; inferenceOutputArray = new byte[1][labelList.size()];
labelProbArray = new float[1][labelList.size()]; labelProbArray = new float[1][labelList.size()];
@ -123,7 +126,8 @@ public class OvicClassifier {
} }
} }
OvicClassificationResult iterResult = computeTopKLabels(); OvicClassificationResult iterResult = computeTopKLabels();
iterResult.latency = getLastNativeInferenceLatencyMilliseconds(); iterResult.latencyMilli = getLastNativeInferenceLatencyMilliseconds();
iterResult.latencyNano = getLastNativeInferenceLatencyNanoseconds();
return iterResult; return iterResult;
} }
@ -154,6 +158,18 @@ public class OvicClassifier {
return (latency == null) ? null : (Long) (latency / 1000000); return (latency == null) ? null : (Long) (latency / 1000000);
} }
/*
* Get native inference latency of last image classification run.
* @throws RuntimeException if model is uninitialized.
*/
public Long getLastNativeInferenceLatencyNanoseconds() {
if (tflite == null) {
throw new IllegalStateException(
TAG + ": ImageNet classifier has not been initialized; Failed.");
}
return tflite.getLastNativeInferenceDurationNanoseconds();
}
/** Closes tflite to release resources. */ /** Closes tflite to release resources. */
public void close() { public void close() {
tflite.close(); tflite.close();
@ -162,9 +178,9 @@ public class OvicClassifier {
/** Reads label list from Assets. */ /** Reads label list from Assets. */
private static List<String> loadLabelList(InputStream labelInputStream) throws IOException { private static List<String> loadLabelList(InputStream labelInputStream) throws IOException {
List<String> labelList = new ArrayList<String>(); List<String> labelList = new ArrayList<>();
try (BufferedReader reader = try (BufferedReader reader =
new BufferedReader(new InputStreamReader(labelInputStream, StandardCharsets.UTF_8))) { new BufferedReader(new InputStreamReader(labelInputStream, UTF_8))) {
String line; String line;
while ((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) {
labelList.add(line); labelList.add(line);

View File

@ -88,16 +88,17 @@ public final class OvicClassifierBenchmarker extends OvicBenchmarker {
Log.e(TAG, e.getMessage()); Log.e(TAG, e.getMessage());
Log.e(TAG, "Failed to classify image."); Log.e(TAG, "Failed to classify image.");
} }
if (iterResult == null || iterResult.latency == null) { if (iterResult == null || iterResult.latencyMilli == null || iterResult.latencyNano == null) {
throw new RuntimeException("Classification result or timing is invalid."); throw new RuntimeException("Classification result or timing is invalid.");
} }
Log.d(TAG, "Native inference latency: " + iterResult.latency); Log.d(TAG, "Native inference latency (ms): " + iterResult.latencyMilli);
Log.d(TAG, "Native inference latency (ns): " + iterResult.latencyNano);
Log.i(TAG, iterResult.toString()); Log.i(TAG, iterResult.toString());
if (!benchmarkStarted) { // Skip the first image to discount warming-up time. if (!benchmarkStarted) { // Skip the first image to discount warming-up time.
benchmarkStarted = true; benchmarkStarted = true;
} else { } else {
totalRuntime += ((double) iterResult.latency); totalRuntimeNano += ((double) iterResult.latencyNano);
} }
return true; return true;
} }

View File

@ -22,7 +22,9 @@ public class OvicDetectionResult {
// Top K classes and probabilities. // Top K classes and probabilities.
public final ArrayList<BoundingBox> detections; public final ArrayList<BoundingBox> detections;
// Latency (ms). // Latency (ms).
public Long latency = -1L; public Long latencyMilli = -1L;
// Latency (ns).
public Long latencyNano = -1L;
// id of the image. // id of the image.
public int id = -1; public int id = -1;
// Number of valid detections (separately maintained, maybe different from detections.size()). // Number of valid detections (separately maintained, maybe different from detections.size()).
@ -37,9 +39,10 @@ public class OvicDetectionResult {
} }
} }
public void resetTo(Long latency, int id) { public void resetTo(Long latencyMilli, Long latencyNano, int id) {
count = 0; count = 0;
this.latency = latency; this.latencyMilli = latencyMilli;
this.latencyNano = latencyNano;
this.id = id; this.id = id;
} }
@ -64,7 +67,8 @@ public class OvicDetectionResult {
@Override @Override
public String toString() { public String toString() {
String textToShow = latency + "ms"; String textToShow = latencyMilli + "ms";
textToShow += "\n" + latencyNano + "ns";
int k = 0; int k = 0;
for (BoundingBox box : detections) { for (BoundingBox box : detections) {
textToShow += textToShow +=

View File

@ -14,13 +14,14 @@ limitations under the License.
==============================================================================*/ ==============================================================================*/
package org.tensorflow.ovic; package org.tensorflow.ovic;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer; import java.nio.MappedByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -105,7 +106,7 @@ public class OvicDetector implements AutoCloseable {
private static List<String> loadLabelList(InputStream labelInputStream) throws IOException { private static List<String> loadLabelList(InputStream labelInputStream) throws IOException {
List<String> labelList = new ArrayList<>(); List<String> labelList = new ArrayList<>();
try (BufferedReader reader = try (BufferedReader reader =
new BufferedReader(new InputStreamReader(labelInputStream, StandardCharsets.UTF_8))) { new BufferedReader(new InputStreamReader(labelInputStream, UTF_8))) {
String line; String line;
while ((line = reader.readLine()) != null) { while ((line = reader.readLine()) != null) {
labelList.add(line); labelList.add(line);
@ -131,10 +132,11 @@ public class OvicDetector implements AutoCloseable {
Object[] inputArray = {imgData}; Object[] inputArray = {imgData};
tflite.runForMultipleInputsOutputs(inputArray, outputMap); tflite.runForMultipleInputsOutputs(inputArray, outputMap);
Long latency = getLastNativeInferenceLatencyMilliseconds(); Long latencyMilli = getLastNativeInferenceLatencyMilliseconds();
Long latencyNano = getLastNativeInferenceLatencyNanoseconds();
// Update the results. // Update the results.
result.resetTo(latency, imageId); result.resetTo(latencyMilli, latencyNano, imageId);
for (int i = 0; i < NUM_RESULTS; i++) { for (int i = 0; i < NUM_RESULTS; i++) {
// The model returns normalized coordinates [start_y, start_x, end_y, end_x]. // The model returns normalized coordinates [start_y, start_x, end_y, end_x].
// The boxes expect pixel coordinates [x1, y1, x2, y2]. // The boxes expect pixel coordinates [x1, y1, x2, y2].
@ -154,7 +156,7 @@ public class OvicDetector implements AutoCloseable {
/* /*
* Get native inference latency of last image detection run. * Get native inference latency of last image detection run.
* @throws RuntimeException if model is uninitialized. * @throws RuntimeException if model is uninitialized.
* @return The inference latency in millisecond. * @return The inference latency in milliseconds.
*/ */
public Long getLastNativeInferenceLatencyMilliseconds() { public Long getLastNativeInferenceLatencyMilliseconds() {
if (tflite == null) { if (tflite == null) {
@ -164,6 +166,19 @@ public class OvicDetector implements AutoCloseable {
return (latency == null) ? null : (Long) (latency / 1000000); return (latency == null) ? null : (Long) (latency / 1000000);
} }
/*
* Get native inference latency of last image detection run.
* @throws RuntimeException if model is uninitialized.
* @return The inference latency in nanoseconds.
*/
public Long getLastNativeInferenceLatencyNanoseconds() {
if (tflite == null) {
throw new IllegalStateException(
TAG + ": ImageNet classifier has not been initialized; Failed.");
}
return tflite.getLastNativeInferenceDurationNanoseconds();
}
public int[] getInputDims() { public int[] getInputDims() {
return inputDims; return inputDims;
} }

View File

@ -98,7 +98,7 @@ public final class OvicDetectorBenchmarker extends OvicBenchmarker {
if (!benchmarkStarted) { // Skip the first image to discount warming-up time. if (!benchmarkStarted) { // Skip the first image to discount warming-up time.
benchmarkStarted = true; benchmarkStarted = true;
} else { } else {
totalRuntime += ((double) detector.result.latency); totalRuntimeNano += ((double) detector.result.latencyNano);
} }
return true; // Indicating that result is ready. return true; // Indicating that result is ready.
} }

View File

@ -116,7 +116,7 @@ public final class OvicClassifierTest {
public void ovicClassifier_latencyNotNull() throws Exception { public void ovicClassifier_latencyNotNull() throws Exception {
classifier = new OvicClassifier(labelsInputStream, floatModel); classifier = new OvicClassifier(labelsInputStream, floatModel);
testResult = classifier.classifyByteBuffer(testImage); testResult = classifier.classifyByteBuffer(testImage);
assertThat(testResult.latency).isNotNull(); assertThat(testResult.latencyNano).isNotNull();
} }
@Test @Test