[java] Adding ability to load a model from a memory mapped byte buffer (#20062)

### Description
Adds support for constructing an `OrtSession` from a
`java.nio.ByteBuffer`. These buffers can be memory mapped from files
which means there doesn't need to be copies of the model protobuf held
in Java, reducing peak memory usage during session construction.

### Motivation and Context
Reduces memory usage on model construction by not requiring as many
copies on the Java side. Should help with #19599.
This commit is contained in:
Adam Pocock 2024-09-15 18:31:55 -04:00 committed by GitHub
parent c63dd0234b
commit 02e00dc023
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 138 additions and 2 deletions

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2023 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2024 Oracle and/or its affiliates. All rights reserved.
* Licensed under the MIT License.
*/
package ai.onnxruntime;
@ -7,6 +7,7 @@ package ai.onnxruntime;
import ai.onnxruntime.OrtSession.SessionOptions;
import ai.onnxruntime.OrtTrainingSession.OrtCheckpointState;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.EnumSet;
import java.util.Objects;
import java.util.logging.Logger;
@ -236,6 +237,52 @@ public final class OrtEnvironment implements AutoCloseable {
return new OrtSession(this, modelPath, allocator, options);
}
/**
* Create a session using the specified {@link SessionOptions}, model and the default memory
* allocator.
*
* @param modelBuffer Byte buffer representing an ONNX model. Must be a direct byte buffer.
* @param options The session options.
* @return An {@link OrtSession} with the specified model.
* @throws OrtException If the model failed to parse, wasn't compatible or caused an error.
*/
public OrtSession createSession(ByteBuffer modelBuffer, SessionOptions options)
throws OrtException {
return createSession(modelBuffer, defaultAllocator, options);
}
/**
* Create a session using the default {@link SessionOptions}, model and the default memory
* allocator.
*
* @param modelBuffer Byte buffer representing an ONNX model. Must be a direct byte buffer.
* @return An {@link OrtSession} with the specified model.
* @throws OrtException If the model failed to parse, wasn't compatible or caused an error.
*/
public OrtSession createSession(ByteBuffer modelBuffer) throws OrtException {
return createSession(modelBuffer, new OrtSession.SessionOptions());
}
/**
* Create a session using the specified {@link SessionOptions} and model buffer.
*
* @param modelBuffer Byte buffer representing an ONNX model. Must be a direct byte buffer.
* @param allocator The memory allocator to use.
* @param options The session options.
* @return An {@link OrtSession} with the specified model.
* @throws OrtException If the model failed to parse, wasn't compatible or caused an error.
*/
OrtSession createSession(ByteBuffer modelBuffer, OrtAllocator allocator, SessionOptions options)
throws OrtException {
Objects.requireNonNull(modelBuffer, "model array must not be null");
if (modelBuffer.remaining() == 0) {
throw new OrtException("Invalid model buffer, no elements remaining.");
} else if (!modelBuffer.isDirect()) {
throw new OrtException("ByteBuffer is not direct.");
}
return new OrtSession(this, modelBuffer, allocator, options);
}
/**
* Create a session using the specified {@link SessionOptions}, model and the default memory
* allocator.

View file

@ -11,6 +11,7 @@ import ai.onnxruntime.providers.OrtCUDAProviderOptions;
import ai.onnxruntime.providers.OrtFlags;
import ai.onnxruntime.providers.OrtTensorRTProviderOptions;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -94,6 +95,31 @@ public class OrtSession implements AutoCloseable {
allocator);
}
/**
* Creates a session reading the model from the supplied byte buffer.
*
* <p>Must be a direct byte buffer.
*
* @param env The environment.
* @param modelBuffer The model protobuf as a byte buffer.
* @param allocator The allocator to use.
* @param options Session configuration options.
* @throws OrtException If the model was corrupted or some other error occurred in native code.
*/
OrtSession(
OrtEnvironment env, ByteBuffer modelBuffer, OrtAllocator allocator, SessionOptions options)
throws OrtException {
this(
createSession(
OnnxRuntime.ortApiHandle,
env.getNativeHandle(),
modelBuffer,
modelBuffer.position(),
modelBuffer.remaining(),
options.getNativeHandle()),
allocator);
}
/**
* Private constructor to build the Java object wrapped around a native session.
*
@ -514,6 +540,15 @@ public class OrtSession implements AutoCloseable {
private static native long createSession(
long apiHandle, long envHandle, byte[] modelArray, long optsHandle) throws OrtException;
private static native long createSession(
long apiHandle,
long envHandle,
ByteBuffer modelBuffer,
int bufferPos,
int bufferSize,
long optsHandle)
throws OrtException;
private native long getNumInputs(long apiHandle, long nativeHandle) throws OrtException;
private native String[] getInputNames(long apiHandle, long nativeHandle, long allocatorHandle)

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2020, 2022 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2024 Oracle and/or its affiliates. All rights reserved.
* Licensed under the MIT License.
*/
#include <jni.h>
@ -48,6 +48,29 @@ JNIEXPORT jlong JNICALL Java_ai_onnxruntime_OrtSession_createSession__JJLjava_la
return (jlong)session;
}
/*
* Class: ai_onnxruntime_OrtSession
* Method: createSession
* Signature: (JJLjava/nio/ByteBuffer;IIJ)J
*/
JNIEXPORT jlong JNICALL Java_ai_onnxruntime_OrtSession_createSession__JJLjava_nio_ByteBuffer_2IIJ(JNIEnv* jniEnv, jclass jclazz, jlong apiHandle, jlong envHandle, jobject buffer, jint bufferPos, jint bufferSize, jlong optsHandle) {
(void)jclazz; // Required JNI parameter not needed by functions which don't need to access their host object.
const OrtApi* api = (const OrtApi*)apiHandle;
OrtEnv* env = (OrtEnv*)envHandle;
OrtSessionOptions* opts = (OrtSessionOptions*)optsHandle;
OrtSession* session = NULL;
// Extract the buffer
char* bufferArr = (char*)(*jniEnv)->GetDirectBufferAddress(jniEnv, buffer);
// Increment by bufferPos bytes
bufferArr = bufferArr + bufferPos;
// Create the session
checkOrtStatus(jniEnv, api, api->CreateSessionFromArray(env, bufferArr, bufferSize, opts, &session));
return (jlong)session;
}
/*
* Class: ai_onnxruntime_OrtSession
* Method: createSession

View file

@ -20,10 +20,14 @@ import ai.onnxruntime.OrtSession.SessionOptions.ExecutionMode;
import ai.onnxruntime.OrtSession.SessionOptions.OptLevel;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.LongBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@ -338,6 +342,33 @@ public class InferenceTest {
}
}
@Test
public void createSessionFromByteBuffer() throws IOException, OrtException {
Path modelPath = TestHelpers.getResourcePath("/squeezenet.onnx");
try (RandomAccessFile file = new RandomAccessFile(modelPath.toFile(), "r");
FileChannel channel = file.getChannel()) {
MappedByteBuffer modelBuffer = channel.map(MapMode.READ_ONLY, 0, channel.size());
try (OrtSession.SessionOptions options = new SessionOptions();
OrtSession session = env.createSession(modelBuffer, options)) {
assertNotNull(session);
assertEquals(1, session.getNumInputs()); // 1 input node
Map<String, NodeInfo> inputInfoList = session.getInputInfo();
assertNotNull(inputInfoList);
assertEquals(1, inputInfoList.size());
NodeInfo input = inputInfoList.get("data_0");
assertEquals("data_0", input.getName()); // input node name
assertTrue(input.getInfo() instanceof TensorInfo);
TensorInfo inputInfo = (TensorInfo) input.getInfo();
assertEquals(OnnxJavaType.FLOAT, inputInfo.type);
int[] expectedInputDimensions = new int[] {1, 3, 224, 224};
assertEquals(expectedInputDimensions.length, inputInfo.shape.length);
for (int i = 0; i < expectedInputDimensions.length; i++) {
assertEquals(expectedInputDimensions[i], inputInfo.shape[i]);
}
}
}
}
@Test
public void createSessionFromByteArray() throws IOException, OrtException {
Path modelPath = TestHelpers.getResourcePath("/squeezenet.onnx");