diff --git a/.gitignore b/.gitignore index 99fcb0e731..85585b2fcd 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,4 @@ java/gradlew java/gradlew.bat java/gradle java/.gradle - +java/hs_*.log diff --git a/java/build.gradle b/java/build.gradle index dd6c7b3b38..7daee81bc1 100644 --- a/java/build.gradle +++ b/java/build.gradle @@ -1,6 +1,7 @@ plugins { id 'java' id 'jacoco' + id 'maven-publish' id 'com.diffplug.gradle.spotless' version '3.26.0' } @@ -13,8 +14,36 @@ allprojects { java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 - withJavadocJar() - withSourcesJar() +} + +project.group = "ai.onnxruntime" +version = rootProject.file('../VERSION_NUMBER').text.trim() + +jar { + into("META-INF/maven/$project.group/$project.name") { + from { generatePomFileForMavenPublication } + rename ".*", "pom.xml" + } +} + +// Add explicit sources jar with pom file. +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = "sources" + from sourceSets.main.allSource + into("META-INF/maven/$project.group/$project.name") { + from { generatePomFileForMavenPublication } + rename ".*", "pom.xml" + } +} + +// Add explicit javadoc jar with pom file +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = "javadoc" + from javadoc.destinationDir + into("META-INF/maven/$project.group/$project.name") { + from { generatePomFileForMavenPublication } + rename ".*", "pom.xml" + } } wrapper { @@ -40,8 +69,6 @@ def cmakeNativeLibDir = "${cmakeJavaDir}/native-lib" def cmakeNativeJniDir = "${cmakeJavaDir}/native-jni" def cmakeBuildOutputDir = "${cmakeJavaDir}/build" -version = rootProject.file('../VERSION_NUMBER').text.trim() - compileJava { options.compilerArgs += ["-h", "${project.buildDir}/headers/"] } @@ -76,6 +103,10 @@ if (cmakeBuildDir != null) { } task allJar(type: Jar) { + into("META-INF/maven/$project.group/$project.name") { + from { generatePomFileForMavenPublication } + rename ".*", "pom.xml" + } classifier = 'all' from sourceSets.main.output from cmakeNativeJniDir @@ -96,7 +127,6 @@ if (cmakeBuildDir != null) { cmakeBuild.dependsOn javadocJar cmakeBuild.dependsOn javadoc - task cmakeCheck(type: Copy) { from project.buildDir include 'reports/**' @@ -116,6 +146,7 @@ test { useJUnitPlatform() testLogging { events "passed", "skipped", "failed" + showStandardStreams = true } } @@ -126,3 +157,34 @@ jacocoTestReport { html.destination file("${buildDir}/jacocoHtml") } } + +publishing { + publications { + maven(MavenPublication) { + groupId = project.group + artifactId = project.name + + from components.java + pom { + name = 'onnx-runtime' + description = 'ONNX Runtime is a performance-focused inference engine for ONNX (Open Neural Network Exchange) models.' + url = 'https://microsoft.github.io/onnxruntime/' + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/licenses/MIT' + } + } + organization { + name = 'Microsoft' + url = 'http://www.microsoft.com' + } + scm { + connection = 'scm:git:git://github.com:microsoft/onnxruntime.git' + developerConnection = 'scm:git:ssh://github.com/microsoft/onnxruntime.git' + url = 'http://github.com/microsoft/onnxruntime' + } + } + } + } +} diff --git a/java/src/main/java/ai/onnxruntime/OnnxModelMetadata.java b/java/src/main/java/ai/onnxruntime/OnnxModelMetadata.java new file mode 100644 index 0000000000..eea45717c7 --- /dev/null +++ b/java/src/main/java/ai/onnxruntime/OnnxModelMetadata.java @@ -0,0 +1,210 @@ +package ai.onnxruntime; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * Contains the metadata associated with an ONNX model. + * + *

Unspecified default fields contain the empty string. + * + *

This class is a Java side copy of the native metadata, it does not access the native runtime. + */ +public final class OnnxModelMetadata { + + private final String producerName; + private final String graphName; + private final String domain; + private final String description; + private final long version; + + private final Map customMetadata; + + /** + * Constructed by an OrtSession in native code, nulls are replaced with the empty string or empty + * map as appropriate. + * + * @param producerName The model producer name. + * @param graphName The model graph name. + * @param domain The model domain name. + * @param description The model description. + * @param version The model version. + * @param customMetadataArray Any custom metadata associated with the model. + */ + OnnxModelMetadata( + String producerName, + String graphName, + String domain, + String description, + long version, + String[] customMetadataArray) { + this.producerName = producerName == null ? "" : producerName; + this.graphName = graphName == null ? "" : graphName; + this.domain = domain == null ? "" : domain; + this.description = description == null ? "" : description; + this.version = version; + if (customMetadataArray != null && customMetadataArray.length > 0) { + this.customMetadata = new HashMap<>(); + if (customMetadataArray.length % 2 == 1) { + throw new IllegalStateException( + "Asked for keys and values, but received an odd number of elements."); + } + for (int i = 0; i < customMetadataArray.length; i += 2) { + customMetadata.put(customMetadataArray[i], customMetadataArray[i + 1]); + } + } else { + this.customMetadata = Collections.emptyMap(); + } + } + + /** + * Constructed by an OrtSession, nulls are replaced with the empty string or empty map as + * appropriate. + * + * @param producerName The model producer name. + * @param graphName The model graph name. + * @param domain The model domain name. + * @param description The model description. + * @param version The model version. + * @param customMetadata Any custom metadata associated with the model. + */ + OnnxModelMetadata( + String producerName, + String graphName, + String domain, + String description, + long version, + Map customMetadata) { + this.producerName = producerName == null ? "" : producerName; + this.graphName = graphName == null ? "" : graphName; + this.domain = domain == null ? "" : domain; + this.description = description == null ? "" : description; + this.version = version; + this.customMetadata = customMetadata == null ? Collections.emptyMap() : customMetadata; + } + + /** + * Copy constructor. + * + * @param other The metadata to copy. + */ + public OnnxModelMetadata(OnnxModelMetadata other) { + this.producerName = other.producerName; + this.graphName = other.graphName; + this.domain = other.domain; + this.description = other.description; + this.version = other.version; + this.customMetadata = + other.customMetadata.isEmpty() + ? Collections.emptyMap() + : new HashMap<>(getCustomMetadata()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OnnxModelMetadata that = (OnnxModelMetadata) o; + return version == that.version + && producerName.equals(that.producerName) + && graphName.equals(that.graphName) + && domain.equals(that.domain) + && description.equals(that.description) + && customMetadata.equals(that.customMetadata); + } + + @Override + public int hashCode() { + return Objects.hash(producerName, graphName, domain, description, version, customMetadata); + } + + /** + * Gets the producer name. + * + * @return The producer name. + */ + public String getProducerName() { + return producerName; + } + + /** + * Gets the graph name. + * + * @return The graph name. + */ + public String getGraphName() { + return graphName; + } + + /** + * Gets the domain. + * + * @return The domain. + */ + public String getDomain() { + return domain; + } + + /** + * Gets the model description. + * + * @return The description. + */ + public String getDescription() { + return description; + } + + /** + * Gets the model version. + * + * @return The model version. + */ + public long getVersion() { + return version; + } + + /** + * Gets an unmodifiable reference to the complete custom metadata. + * + * @return The custom metadata. + */ + public Map getCustomMetadata() { + return Collections.unmodifiableMap(customMetadata); + } + + /** + * Returns Optional.of(value) if the custom metadata has a value for the supplied key, otherwise + * returns {@link Optional#empty}. + * + * @param key The custom metadata key. + * @return The custom metadata value if present. + */ + public Optional getCustomMetadataValue(String key) { + return Optional.ofNullable(customMetadata.get(key)); + } + + @Override + public String toString() { + return "OnnxModelMetadata{" + + "producerName='" + + producerName + + '\'' + + ", graphName='" + + graphName + + '\'' + + ", domain='" + + domain + + '\'' + + ", description='" + + description + + '\'' + + ", version=" + + version + + ", customMetadata=" + + customMetadata + + '}'; + } +} diff --git a/java/src/main/java/ai/onnxruntime/OnnxRuntime.java b/java/src/main/java/ai/onnxruntime/OnnxRuntime.java index 18f9c397bb..83ea79dd4e 100644 --- a/java/src/main/java/ai/onnxruntime/OnnxRuntime.java +++ b/java/src/main/java/ai/onnxruntime/OnnxRuntime.java @@ -24,6 +24,8 @@ final class OnnxRuntime { private static final int ORT_API_VERSION_1 = 1; // Post 1.0 builds of the ORT API. private static final int ORT_API_VERSION_2 = 2; + // Post 1.3 builds of the ORT API + private static final int ORT_API_VERSION_3 = 3; /** The short name of the ONNX runtime shared library */ static final String ONNXRUNTIME_LIBRARY_NAME = "onnxruntime"; @@ -50,7 +52,7 @@ final class OnnxRuntime { try { load(tempDirectory, ONNXRUNTIME_LIBRARY_NAME); load(tempDirectory, ONNXRUNTIME_JNI_LIBRARY_NAME); - ortApiHandle = initialiseAPIBase(ORT_API_VERSION_2); + ortApiHandle = initialiseAPIBase(ORT_API_VERSION_3); loaded = true; } finally { if (!isAndroid()) { diff --git a/java/src/main/java/ai/onnxruntime/OrtSession.java b/java/src/main/java/ai/onnxruntime/OrtSession.java index 80a1d78fa4..6483842aba 100644 --- a/java/src/main/java/ai/onnxruntime/OrtSession.java +++ b/java/src/main/java/ai/onnxruntime/OrtSession.java @@ -47,6 +47,8 @@ public class OrtSession implements AutoCloseable { private final Set outputNames; + private OnnxModelMetadata metadata; + private boolean closed = false; /** @@ -201,7 +203,7 @@ public class OrtSession implements AutoCloseable { /** * Scores an input feed dict, returning the map of requested inferred outputs. * - *

The outputs are sorted based on the supplied set traveral order. + *

The outputs are sorted based on the supplied set traversal order. * * @param inputs The inputs to score. * @param requestedOutputs The requested outputs. @@ -263,6 +265,18 @@ public class OrtSession implements AutoCloseable { } } + /** + * Gets the metadata for the currently loaded model. + * + * @return The metadata. + */ + public OnnxModelMetadata getMetadata() throws OrtException { + if (metadata == null) { + metadata = constructMetadata(OnnxRuntime.ortApiHandle, nativeHandle, allocator.handle); + } + return metadata; + } + @Override public String toString() { return "OrtSession(numInputs=" + numInputs + ",numOutputs=" + numOutputs + ")"; @@ -334,6 +348,18 @@ public class OrtSession implements AutoCloseable { private native void closeSession(long apiHandle, long nativeHandle) throws OrtException; + /** + * Builds the {@link OnnxModelMetadata} for this session. + * + * @param ortApiHandle The api pointer. + * @param nativeHandle The native session pointer. + * @param allocatorHandle The OrtAllocator pointer. + * @return The metadata. + * @throws OrtException If the native runtime failed to access or allocate the metadata. + */ + private native OnnxModelMetadata constructMetadata( + long ortApiHandle, long nativeHandle, long allocatorHandle) throws OrtException; + /** * Represents the options used to construct this session. * diff --git a/java/src/main/native/ai_onnxruntime_OrtSession.c b/java/src/main/native/ai_onnxruntime_OrtSession.c index e0eeec63dd..2111d16746 100644 --- a/java/src/main/native/ai_onnxruntime_OrtSession.c +++ b/java/src/main/native/ai_onnxruntime_OrtSession.c @@ -337,3 +337,94 @@ JNIEXPORT void JNICALL Java_ai_onnxruntime_OrtSession_releaseNamesHandle } checkOrtStatus(jniEnv,api,api->AllocatorFree(allocator,names)); } + +/* + * Class: ai_onnxruntime_OrtSession + * Method: constructMetadata + * Signature: (JJJ)Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_ai_onnxruntime_OrtSession_constructMetadata + (JNIEnv * jniEnv, jobject jobj, jlong apiHandle, jlong nativeHandle, jlong allocatorHandle) { + (void) jobj; // Required JNI parameter not needed by functions which don't need to access their host object. + const OrtApi* api = (const OrtApi*) apiHandle; + OrtAllocator* allocator = (OrtAllocator*) allocatorHandle; + + // Setup + char* stringClassName = "java/lang/String"; + jclass stringClazz = (*jniEnv)->FindClass(jniEnv, stringClassName); + char *metadataClassName = "ai/onnxruntime/OnnxModelMetadata"; + jclass metadataClazz = (*jniEnv)->FindClass(jniEnv, metadataClassName); + //OnnxModelMetadata(String producerName, String graphName, String domain, String description, long version, String[] customMetadataArray) + jmethodID metadataConstructor = (*jniEnv)->GetMethodID(jniEnv, metadataClazz, "", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;J[Ljava/lang/String;)V"); + + // Get metadata + OrtModelMetadata* metadata; + checkOrtStatus(jniEnv,api,api->SessionGetModelMetadata((OrtSession*)nativeHandle,&metadata)); + + // Read out the producer name and convert it to a java.lang.String + char* charBuffer; + checkOrtStatus(jniEnv,api,api->ModelMetadataGetProducerName(metadata, allocator, &charBuffer)); + jstring producerStr = (*jniEnv)->NewStringUTF(jniEnv,charBuffer); + checkOrtStatus(jniEnv,api,api->AllocatorFree(allocator,charBuffer)); + + // Read out the graph name and convert it to a java.lang.String + checkOrtStatus(jniEnv,api,api->ModelMetadataGetGraphName(metadata, allocator, &charBuffer)); + jstring graphStr = (*jniEnv)->NewStringUTF(jniEnv,charBuffer); + checkOrtStatus(jniEnv,api,api->AllocatorFree(allocator,charBuffer)); + + // Read out the domain and convert it to a java.lang.String + checkOrtStatus(jniEnv,api,api->ModelMetadataGetDomain(metadata, allocator, &charBuffer)); + jstring domainStr = (*jniEnv)->NewStringUTF(jniEnv,charBuffer); + checkOrtStatus(jniEnv,api,api->AllocatorFree(allocator,charBuffer)); + + // Read out the description and convert it to a java.lang.String + checkOrtStatus(jniEnv,api,api->ModelMetadataGetDescription(metadata, allocator, &charBuffer)); + jstring descriptionStr = (*jniEnv)->NewStringUTF(jniEnv,charBuffer); + checkOrtStatus(jniEnv,api,api->AllocatorFree(allocator,charBuffer)); + + // Read out the version + int64_t version; + checkOrtStatus(jniEnv,api,api->ModelMetadataGetVersion(metadata, &version)); + + // Read out the keys, look up the values. + int64_t numKeys; + char** keys; + checkOrtStatus(jniEnv,api,api->ModelMetadataGetCustomMetadataMapKeys(metadata, allocator, &keys, &numKeys)); + jobjectArray customArray = NULL; + if (numKeys > 0) { + customArray = (*jniEnv)->NewObjectArray(jniEnv,numKeys*2,stringClazz,NULL); + + // Iterate key array to extract the values + for (int64_t i = 0; i < numKeys; i++) { + // Create a java.lang.String for the key + jstring keyJava = (*jniEnv)->NewStringUTF(jniEnv,keys[i]); + + // Extract the value and convert it to a java.lang.String + checkOrtStatus(jniEnv,api,api->ModelMetadataLookupCustomMetadataMap(metadata,allocator,keys[i],&charBuffer)); + jstring valueJava = (*jniEnv)->NewStringUTF(jniEnv,charBuffer); + checkOrtStatus(jniEnv,api,api->AllocatorFree(allocator,charBuffer)); + + // Write the key and value into the array + (*jniEnv)->SetObjectArrayElement(jniEnv, customArray, i*2, keyJava); + (*jniEnv)->SetObjectArrayElement(jniEnv, customArray, (i*2)+1, valueJava); + } + + // Release key array + for (int64_t i = 0; i < numKeys; i++) { + checkOrtStatus(jniEnv,api,api->AllocatorFree(allocator,keys[i])); + } + checkOrtStatus(jniEnv,api,api->AllocatorFree(allocator,keys)); + } else { + customArray = (*jniEnv)->NewObjectArray(jniEnv,0,stringClazz,NULL); + } + + // Invoke the metadata constructor + //OnnxModelMetadata(String producerName, String graphName, String domain, String description, long version, String[] customMetadataArray) + jobject metadataJava = (*jniEnv)->NewObject(jniEnv, metadataClazz, metadataConstructor, producerStr, graphStr, domainStr, descriptionStr, (jlong) version, customArray); + + // Release the metadata + api->ReleaseModelMetadata(metadata); + + return metadataJava; +} diff --git a/java/src/test/java/ai/onnxruntime/InferenceTest.java b/java/src/test/java/ai/onnxruntime/InferenceTest.java index d0521d9429..5423df20a1 100644 --- a/java/src/test/java/ai/onnxruntime/InferenceTest.java +++ b/java/src/test/java/ai/onnxruntime/InferenceTest.java @@ -41,6 +41,8 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -63,6 +65,7 @@ public class InferenceTest { @Test public void repeatedCloseTest() throws OrtException { + Logger.getLogger(OrtEnvironment.class.getName()).setLevel(Level.SEVERE); OrtEnvironment env = OrtEnvironment.getEnvironment("repeatedCloseTest"); try (OrtEnvironment otherEnv = OrtEnvironment.getEnvironment()) { assertFalse(otherEnv.isClosed()); @@ -115,7 +118,9 @@ public class InferenceTest { @Test public void morePartialInputsTest() throws OrtException { String modelPath = getResourcePath("/partial-inputs-test-2.onnx").toString(); - try (OrtEnvironment env = OrtEnvironment.getEnvironment("partialInputs"); + try (OrtEnvironment env = + OrtEnvironment.getEnvironment( + OrtEnvironment.LoggingLevel.ORT_LOGGING_LEVEL_FATAL, "partialInputs"); OrtSession.SessionOptions options = new SessionOptions(); OrtSession session = env.createSession(modelPath, options)) { assertNotNull(session); @@ -200,7 +205,9 @@ public class InferenceTest { @Test public void partialInputsTest() throws OrtException { String modelPath = getResourcePath("/partial-inputs-test.onnx").toString(); - try (OrtEnvironment env = OrtEnvironment.getEnvironment("partialInputs"); + try (OrtEnvironment env = + OrtEnvironment.getEnvironment( + OrtEnvironment.LoggingLevel.ORT_LOGGING_LEVEL_FATAL, "partialInputs"); OrtSession.SessionOptions options = new SessionOptions(); OrtSession session = env.createSession(modelPath, options)) { assertNotNull(session); @@ -370,6 +377,15 @@ public class InferenceTest { for (int i = 0; i < expectedOutputDimensions.length; i++) { assertEquals(expectedOutputDimensions[i], outputInfo.shape[i]); } + + // Check the metadata can be extracted + OnnxModelMetadata metadata = session.getMetadata(); + assertEquals("onnx-caffe2", metadata.getProducerName()); + assertEquals("squeezenet_old", metadata.getGraphName()); + assertEquals("", metadata.getDomain()); + assertEquals("", metadata.getDescription()); + assertEquals(0x7FFFFFFFFFFFFFFFL, metadata.getVersion()); + assertTrue(metadata.getCustomMetadata().isEmpty()); } } }