TensorFlow
Java
Multi-GPU
Inference
Machine Learning

Tensorflow Java Multi-GPU inference

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

Introduction

Running inference across multiple GPUs in TensorFlow Java requires planning beyond simple model loading. Java bindings can execute TensorFlow graphs efficiently, but parallel device utilization depends on session usage patterns and deployment design. A practical strategy is to run concurrent inference workers with controlled batching.

Understand How Device Placement Works

TensorFlow places operations on available devices based on graph placement rules. In Java, this behavior is inherited from the loaded model and runtime configuration. If one process issues requests serially, only part of available GPU capacity may be used.

For high throughput, combine parallel request handling with batch-friendly model execution.

Load Model Once and Serve Concurrent Requests

A basic TensorFlow Java inference service can reuse one loaded model and process requests via a thread pool.

java
1import org.tensorflow.SavedModelBundle;
2import org.tensorflow.Tensor;
3
4import java.nio.FloatBuffer;
5import java.util.concurrent.ExecutorService;
6import java.util.concurrent.Executors;
7
8public class InferenceService {
9    private final SavedModelBundle model;
10    private final ExecutorService pool;
11
12    public InferenceService(String modelDir, int workers) {
13        this.model = SavedModelBundle.load(modelDir, "serve");
14        this.pool = Executors.newFixedThreadPool(workers);
15    }
16
17    public float runSingle(float[] input) {
18        try (Tensor<Float> x = Tensor.of(Float.class, org.tensorflow.ndarray.Shape.of(1, input.length),
19                data -> data.write(FloatBuffer.wrap(input)))) {
20            try (Tensor<?> out = model.session().runner()
21                    .feed("serving_default_input", x)
22                    .fetch("StatefulPartitionedCall")
23                    .run()
24                    .get(0)) {
25                float[] y = new float[1];
26                out.rawData().asFloats().read(y);
27                return y[0];
28            }
29        }
30    }
31}

The exact input and output names depend on your exported model signature.

Scale with Worker Processes for Multi-GPU Usage

One robust pattern is process-level scaling, where each process is pinned to a GPU using environment settings. A load balancer routes requests across worker processes. This avoids thread contention and gives predictable memory allocation per device.

In container deployments, each worker can run with one assigned GPU and independent health checks.

Batch Requests for Better Throughput

If latency budget allows, micro-batch incoming requests before inference calls. GPUs are generally more efficient with batch workloads than many tiny single-item runs. Implement queue-based batching with max batch size and max wait time constraints.

This approach can significantly improve throughput while keeping latency within service objectives.

Operational Monitoring Strategy

Monitor GPU utilization, queue depth, request latency, and model execution time. If utilization is low, increase worker count or batch size. If latency spikes, lower batch limits or adjust concurrency. Continuous measurement is essential because optimal settings vary by model architecture and hardware.

Add per-model metrics tags so you can tune each model profile independently in mixed workloads.

Deployment Pattern Example

A common deployment shape is one JVM process per GPU, each exposing a local inference endpoint. An upstream router distributes requests using least-load or round-robin policies. This pattern simplifies GPU ownership and reduces cross-device memory contention.

It also makes rolling updates safer because each worker can be drained and replaced independently.

Request Routing and Backpressure

Multi-GPU systems need routing logic that avoids hot spots. Add queue limits and backpressure so one busy worker does not accumulate unbounded requests.

A lightweight router can track in-flight requests per worker and send traffic to the least-loaded target. This improves tail latency and avoids memory spikes during bursts.

Validation and Load Testing

Before production rollout, run controlled load tests that reflect expected traffic patterns. Measure throughput, p95 latency, and GPU utilization at different batch sizes. Use those results to set default worker and batching parameters.

Without load testing, configuration choices are often guesswork and may underuse available hardware.

Common Pitfalls

  • Assuming model load alone will automatically saturate multiple GPUs.
  • Running all requests serially through one execution path.
  • Ignoring model signature names and feeding wrong tensor shapes.
  • Scaling concurrency without tracking latency and memory pressure.

Summary

  • TensorFlow Java can support multi-GPU inference with proper serving architecture.
  • Use concurrent workers and batching to improve device utilization.
  • Consider process-level GPU pinning for predictable scaling.
  • Tune with real metrics instead of fixed assumptions.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

All Rights Reserved.