This is an automated email from the ASF dual-hosted git repository.
damccorm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new c102eb4c321 ML pipelines: RunInference - OSS Image Object detection,
OSS Image Captioning, OSS Image Classification (#37186)
c102eb4c321 is described below
commit c102eb4c32169d724c1d51a4f36e2925a0714203
Author: Vitaly Terentyev <[email protected]>
AuthorDate: Fri Jul 17 15:49:23 2026 +0400
ML pipelines: RunInference - OSS Image Object detection, OSS Image
Captioning, OSS Image Classification (#37186)
* ML pipelines: RunInference - OSS Image Object detection
* Refactoring
* Add PyTorch Image Captioning BLIP + CLIP Batch
* Refactoring
* Add PyTorch Image Classification EfficientNet-B0 Streaming
(Right-fitting) ML pipeline
* Fix lint
* Rename files
* Add project argument
* Fix inference_fn
* Fix inference result
* Update torch for image captioning
* Fix inference
* Add Looker ids
* Fix float for imagenet rightfit
* Add all pipelines to separate workflow
* Fix streaming mode
* Fix clip blip
* Fix quotes
* Fix formatting
* Fix lint
* Add Looker IDs
* Resolve comments [1]
* Resolve comments [2]
* Resolve comments [3]
* Resolve comments [4]
* Fix conflicts
* Fix formatting
* Remove rate limit
* Refactor model and process init
* Add beam_Inference_Python_Benchmarks_Dataflow_2 to README.md
* Refactoring
* Move to one yml file
* Resolve comments
* Resolve conflicts
* Resolve comments
* Fix formatter
* Resolve comments
* Change default batch size
* Fix formatting
---
.../beam_Inference_Python_Benchmarks_Dataflow.yml | 153 ++++-
...enchmarks_Dataflow_Pytorch_Image_Captioning.txt | 37 ++
...aflow_Pytorch_Image_Classification_Rightfit.txt | 41 ++
...rks_Dataflow_Pytorch_Image_Object_Detection.txt | 37 ++
.test-infra/tools/refresh_looker_metrics.py | 19 +
.../examples/inference/pytorch_image_captioning.py | 641 +++++++++++++++++++++
.../inference/pytorch_image_object_detection.py | 517 +++++++++++++++++
.../inference/pytorch_imagenet_rightfit.py | 537 +++++++++++++++++
.../pytorch_image_captioning_requirements.txt | 27 +
...pytorch_image_object_detection_requirements.txt | 24 +
.../ml/inference/pytorch_rightfit_requirements.txt | 26 +
.../pytorch_image_captioning_benchmarks.py | 42 ++
.../pytorch_image_object_detection_benchmarks.py | 42 ++
.../pytorch_imagenet_rightfit_benchmarks.py | 42 ++
website/www/site/content/en/performance/_index.md | 12 +
.../pytorchimagecaptioningbatchcpu/_index.md | 41 ++
.../pytorchimagecaptioningbatchgpu/_index.md | 41 ++
.../pytorchimagecaptioningstreamingcpu/_index.md | 41 ++
.../pytorchimagecaptioningstreaminggpu/_index.md | 41 ++
.../pytorchimagenetrightfitcpu/_index.md | 43 ++
.../pytorchimagenetrightfitgpu/_index.md | 43 ++
.../pytorchimagenetrightfitoncecpu/_index.md | 44 ++
.../pytorchimagenetrightfitoncegpu/_index.md | 44 ++
.../pytorchimageobjectdetectionbatchcpu/_index.md | 41 ++
.../pytorchimageobjectdetectionbatchgpu/_index.md | 41 ++
.../_index.md | 41 ++
.../_index.md | 41 ++
website/www/site/data/performance.yaml | 206 ++++++-
28 files changed, 2889 insertions(+), 16 deletions(-)
diff --git a/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml
b/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml
index ce3017f40b0..fb7fecd2c01 100644
--- a/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml
+++ b/.github/workflows/beam_Inference_Python_Benchmarks_Dataflow.yml
@@ -55,7 +55,7 @@ jobs:
(github.event_name == 'schedule' && github.repository == 'apache/beam')
||
github.event.comment.body == 'Run Inference Benchmarks'
runs-on: [self-hosted, ubuntu-24.04, main]
- timeout-minutes: 1000
+ timeout-minutes: 3000
name: ${{ matrix.job_name }} (${{ matrix.job_phrase }})
strategy:
matrix:
@@ -96,6 +96,9 @@ jobs:
${{ github.workspace
}}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_VLLM_Gemma_Batch.txt
${{ github.workspace
}}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Table_Row_Inference_Batch.txt
${{ github.workspace
}}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Table_Row_Inference_Stream.txt
+ ${{ github.workspace
}}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Classification_Rightfit.txt
+ ${{ github.workspace
}}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Object_Detection.txt
+ ${{ github.workspace
}}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Captioning.txt
${{ github.workspace
}}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Generate_Vocab_Batch.txt
${{ github.workspace
}}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_One_Hot_Encoding_Batch.txt
${{ github.workspace
}}/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_MLTransform_Image_Embedding_GPU_Batch.txt
@@ -198,7 +201,7 @@ jobs:
-Prunner=DataflowRunner \
-PpythonVersion=3.10 \
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/torch_tests_requirements.txt
\
- '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_5 }}
--job_name=benchmark-tests-pytorch-imagenet-python-gpu-${{env.NOW_UTC}}
--output=gs://temp-storage-for-end-to-end-tests/torch/result_resnet152_gpu-${{env.NOW_UTC}}.txt'
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_5 }}
--job_name=benchmark-tests-pytorch-imagenet-python-gpu-${{env.NOW_UTC}}
--output=gs://temp-storage-for-end-to-end-tests/torch/result_resnet152_gpu-${{env.NOW_UTC}}.txt'
\
- name: run Table Row Inference Sklearn Batch
uses: ./.github/actions/gradle-command-self-hosted-action
timeout-minutes: 180
@@ -209,7 +212,7 @@ jobs:
-Prunner=DataflowRunner \
-PpythonVersion=3.10 \
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/table_row_inference_requirements.txt
\
- '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_9 }}
--autoscaling_algorithm=NONE --metrics_table=result_table_row_inference_batch
--influx_measurement=result_table_row_inference_batch --mode=batch
--input_file=gs://apache-beam-ml/testing/inputs/table_rows_100k_benchmark.jsonl
--input_expand_factor=100
--output_table=apache-beam-testing:beam_run_inference.result_table_row_inference_batch_outputs
--job_name=benchmark-tests-table-row-inference- [...]
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_9 }}
--autoscaling_algorithm=NONE --metrics_table=result_table_row_inference_batch
--influx_measurement=result_table_row_inference_batch --mode=batch
--input_file=gs://apache-beam-ml/testing/inputs/table_rows_100k_benchmark.jsonl
--input_expand_factor=100
--output_table=apache-beam-testing:beam_run_inference.result_table_row_inference_batch_outputs
--job_name=benchmark-tests-table-row-inference- [...]
- name: run Table Row Inference Sklearn Stream
uses: ./.github/actions/gradle-command-self-hosted-action
timeout-minutes: 180
@@ -220,7 +223,139 @@ jobs:
-Prunner=DataflowRunner \
-PpythonVersion=3.10 \
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/table_row_inference_requirements.txt
\
- '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_10 }}
--autoscaling_algorithm=THROUGHPUT_BASED --max_num_workers=20
--metrics_table=result_table_row_inference_stream
--influx_measurement=result_table_row_inference_stream --mode=streaming
--input_subscription=projects/apache-beam-testing/subscriptions/table_row_inference_benchmark
--window_size_sec=60 --trigger_interval_sec=30 --timeout_ms=900000
--output_table=apache-beam-testing:beam_run_inf [...]
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_10 }}
--autoscaling_algorithm=THROUGHPUT_BASED --max_num_workers=20
--metrics_table=result_table_row_inference_stream
--influx_measurement=result_table_row_inference_stream --mode=streaming
--input_subscription=projects/apache-beam-testing/subscriptions/table_row_inference_benchmark
--window_size_sec=60 --trigger_interval_sec=30 --timeout_ms=900000
--output_table=apache-beam-testing:beam_run_inf [...]
+ - name: run PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch
CPU
+ uses: ./.github/actions/gradle-command-self-hosted-action
+ timeout-minutes: 180
+ with:
+ gradle-command: :sdks:python:apache_beam:testing:load_tests:run
+ arguments: |
+
-PloadTest.mainClass=apache_beam.testing.benchmarks.inference.pytorch_image_object_detection_benchmarks
\
+ -Prunner=DataflowRunner \
+ -PpythonVersion=3.10 \
+
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_object_detection_requirements.txt
\
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_12 }} --device=CPU
--mode=batch
--job_name=benchmark-tests-pytorch-image-object-detection-batch-${{env.NOW_UTC}}
--metrics_table=torch_inference_image_object_detection_batch_cpu
--output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_object_detection_batch_cpu'
\
+ - name: run PyTorch Image Captioning BLIP + CLIP Batch CPU
+ uses: ./.github/actions/gradle-command-self-hosted-action
+ timeout-minutes: 180
+ with:
+ gradle-command: :sdks:python:apache_beam:testing:load_tests:run
+ arguments: |
+
-PloadTest.mainClass=apache_beam.testing.benchmarks.inference.pytorch_image_captioning_benchmarks
\
+ -Prunner=DataflowRunner \
+ -PpythonVersion=3.10 \
+
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_captioning_requirements.txt
\
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_13 }} --device=CPU
--mode=batch
--job_name=benchmark-tests-pytorch-image-captioning-batch-${{env.NOW_UTC}}
--metrics_table=torch_inference_image_captioning_batch_cpu
--output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_captioning_batch_cpu'
+ - name: run PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch
GPU
+ uses: ./.github/actions/gradle-command-self-hosted-action
+ timeout-minutes: 180
+ with:
+ gradle-command: :sdks:python:apache_beam:testing:load_tests:run
+ arguments: |
+
-PloadTest.mainClass=apache_beam.testing.benchmarks.inference.pytorch_image_object_detection_benchmarks
\
+ -Prunner=DataflowRunner \
+ -PpythonVersion=3.10 \
+
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_object_detection_requirements.txt
\
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_12 }} --device=GPU
--worker_accelerator=type=nvidia-tesla-t4,count=1,install-nvidia-driver=true
--mode=batch
--job_name=benchmark-tests-pytorch-image-object-detection-batch-${{env.NOW_UTC}}
--metrics_table=torch_inference_image_object_detection_batch_gpu
--output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_object_detection_batch_gpu'
\
+ - name: run PyTorch Image Captioning BLIP + CLIP Batch GPU
+ uses: ./.github/actions/gradle-command-self-hosted-action
+ timeout-minutes: 180
+ with:
+ gradle-command: :sdks:python:apache_beam:testing:load_tests:run
+ arguments: |
+
-PloadTest.mainClass=apache_beam.testing.benchmarks.inference.pytorch_image_captioning_benchmarks
\
+ -Prunner=DataflowRunner \
+ -PpythonVersion=3.10 \
+
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_captioning_requirements.txt
\
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_13 }} --device=GPU
--worker_accelerator=type=nvidia-tesla-t4,count=1,install-nvidia-driver=true
--mode=batch
--job_name=benchmark-tests-pytorch-image-captioning-batch-${{env.NOW_UTC}}
--metrics_table=torch_inference_image_captioning_batch_gpu
--output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_captioning_batch_gpu'
\
+ - name: run PyTorch Image Object Detection Faster R-CNN ResNet-50
Streaming CPU
+ uses: ./.github/actions/gradle-command-self-hosted-action
+ timeout-minutes: 180
+ with:
+ gradle-command: :sdks:python:apache_beam:testing:load_tests:run
+ arguments: |
+
-PloadTest.mainClass=apache_beam.testing.benchmarks.inference.pytorch_image_object_detection_benchmarks
\
+ -Prunner=DataflowRunner \
+ -PpythonVersion=3.10 \
+
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_object_detection_requirements.txt
\
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_12 }} --device=CPU
--mode=streaming
--job_name=benchmark-tests-pytorch-image-object-detection-streaming-${{env.NOW_UTC}}
--metrics_table=torch_inference_image_object_detection_streaming_cpu
--output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_object_detection_streaming_cpu'
\
+ - name: run PyTorch Image Captioning BLIP + CLIP Streaming CPU
+ uses: ./.github/actions/gradle-command-self-hosted-action
+ timeout-minutes: 180
+ with:
+ gradle-command: :sdks:python:apache_beam:testing:load_tests:run
+ arguments: |
+
-PloadTest.mainClass=apache_beam.testing.benchmarks.inference.pytorch_image_captioning_benchmarks
\
+ -Prunner=DataflowRunner \
+ -PpythonVersion=3.10 \
+
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_captioning_requirements.txt
\
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_13 }} --device=CPU
--mode=streaming
--job_name=benchmark-tests-pytorch-image-captioning-streaming-${{env.NOW_UTC}}
--metrics_table=torch_inference_image_captioning_streaming_cpu
--output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_captioning_streaming_cpu'
+ - name: run PyTorch Image Object Detection Faster R-CNN ResNet-50
Streaming GPU
+ uses: ./.github/actions/gradle-command-self-hosted-action
+ timeout-minutes: 180
+ with:
+ gradle-command: :sdks:python:apache_beam:testing:load_tests:run
+ arguments: |
+
-PloadTest.mainClass=apache_beam.testing.benchmarks.inference.pytorch_image_object_detection_benchmarks
\
+ -Prunner=DataflowRunner \
+ -PpythonVersion=3.10 \
+
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_object_detection_requirements.txt
\
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_12 }} --device=GPU
--worker_accelerator=type=nvidia-tesla-t4,count=1,install-nvidia-driver=true
--mode=streaming
--job_name=benchmark-tests-pytorch-image-object-detection-streaming-${{env.NOW_UTC}}
--metrics_table=torch_inference_image_object_detection_streaming_gpu
--output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_object_detection_streaming_gpu'
\
+ - name: run PyTorch Image Captioning BLIP + CLIP Streaming GPU
+ uses: ./.github/actions/gradle-command-self-hosted-action
+ timeout-minutes: 180
+ with:
+ gradle-command: :sdks:python:apache_beam:testing:load_tests:run
+ arguments: |
+
-PloadTest.mainClass=apache_beam.testing.benchmarks.inference.pytorch_image_captioning_benchmarks
\
+ -Prunner=DataflowRunner \
+ -PpythonVersion=3.10 \
+
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_image_captioning_requirements.txt
\
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_13 }} --device=GPU
--worker_accelerator=type=nvidia-tesla-t4,count=1,install-nvidia-driver=true
--mode=streaming
--job_name=benchmark-tests-pytorch-image-captioning-streaming-${{env.NOW_UTC}}
--metrics_table=torch_inference_image_captioning_streaming_gpu
--output_table=apache-beam-testing.beam_run_inference.result_torch_inference_image_captioning_streaming_gpu'
\
+ - name: run PyTorch Image Classification EfficientNet-B0 Streaming
(Right-fitting) CPU
+ uses: ./.github/actions/gradle-command-self-hosted-action
+ timeout-minutes: 180
+ with:
+ gradle-command: :sdks:python:apache_beam:testing:load_tests:run
+ arguments: |
+
-PloadTest.mainClass=apache_beam.testing.benchmarks.inference.pytorch_imagenet_rightfit_benchmarks
\
+ -Prunner=DataflowRunner \
+ -PpythonVersion=3.10 \
+
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_rightfit_requirements.txt
\
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_11 }} --device=CPU
--experiments=enable_streaming_rightfitting --mode=streaming
--job_name=benchmark-tests-pytorch-imagenet-rightfit-streaming-${{env.NOW_UTC}}
--metrics_table=torch_inference_imagenet_stream_rightfit_cpu
--output_table=apache-beam-testing.beam_run_inference.result_torch_inference_imagenet_stream_rightfit_cpu'
\
+ - name: run PyTorch Image Classification EfficientNet-B0 Streaming
(Right-fitting Exactly-once) CPU
+ uses: ./.github/actions/gradle-command-self-hosted-action
+ timeout-minutes: 180
+ with:
+ gradle-command: :sdks:python:apache_beam:testing:load_tests:run
+ arguments: |
+
-PloadTest.mainClass=apache_beam.testing.benchmarks.inference.pytorch_imagenet_rightfit_benchmarks
\
+ -Prunner=DataflowRunner \
+ -PpythonVersion=3.10 \
+
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_rightfit_requirements.txt
\
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_11 }} --device=CPU
--experiments=enable_streaming_rightfitting --mode=streaming
--job_name=benchmark-tests-pytorch-imagenet-rightfit-streaming-${{env.NOW_UTC}}
--metrics_table=torch_inference_imagenet_stream_rightfit_once_cpu
--output_table=apache-beam-testing.beam_run_inference.result_torch_inference_imagenet_stream_rightfit_once_cpu'
\
+ - name: run PyTorch Image Classification EfficientNet-B0 Streaming
(Right-fitting) GPU
+ uses: ./.github/actions/gradle-command-self-hosted-action
+ timeout-minutes: 180
+ with:
+ gradle-command: :sdks:python:apache_beam:testing:load_tests:run
+ arguments: |
+
-PloadTest.mainClass=apache_beam.testing.benchmarks.inference.pytorch_imagenet_rightfit_benchmarks
\
+ -Prunner=DataflowRunner \
+ -PpythonVersion=3.10 \
+
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_rightfit_requirements.txt
\
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_11 }} --device=GPU
--experiments=enable_streaming_rightfitting,worker_accelerator=type:nvidia-tesla-t4;count:1;install-nvidia-driver:5xx
--mode=streaming
--job_name=benchmark-tests-pytorch-imagenet-rightfit-streaming-${{env.NOW_UTC}}
--metrics_table=torch_inference_imagenet_stream_rightfit_gpu
--output_table=apache-beam-testing.beam_run_inference.result_torch_inference_imagenet_stream_rightfit_gpu'
\
+ - name: run PyTorch Image Classification EfficientNet-B0 Streaming
(Right-fitting Exactly-once) GPU
+ uses: ./.github/actions/gradle-command-self-hosted-action
+ timeout-minutes: 180
+ with:
+ gradle-command: :sdks:python:apache_beam:testing:load_tests:run
+ arguments: |
+
-PloadTest.mainClass=apache_beam.testing.benchmarks.inference.pytorch_imagenet_rightfit_benchmarks
\
+ -Prunner=DataflowRunner \
+ -PpythonVersion=3.10 \
+
-PloadTest.requirementsTxtFile=apache_beam/ml/inference/pytorch_rightfit_requirements.txt
\
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_11 }} --device=GPU
--experiments=enable_streaming_rightfitting,worker_accelerator=type:nvidia-tesla-t4;count:1;install-nvidia-driver:5xx
--mode=streaming
--job_name=benchmark-tests-pytorch-imagenet-rightfit-streaming-${{env.NOW_UTC}}
--metrics_table=torch_inference_imagenet_stream_rightfit_once_gpu
--output_table=apache-beam-testing.beam_run_inference.result_torch_inference_imagenet_stream_right
[...]
- name: run MLTransform Generate Vocab Batch
uses: ./.github/actions/gradle-command-self-hosted-action
timeout-minutes: 180
@@ -231,7 +366,7 @@ jobs:
-Prunner=DataflowRunner \
-PpythonVersion=3.10 \
-PloadTest.requirementsTxtFile=apache_beam/examples/ml_transform/mltransform_generate_vocab_requirements.txt
\
- '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_11 }}
--job_name=benchmark-tests-mltransform-generate-vocab-batch-${{env.NOW_UTC}}
--artifact_location=gs://temp-storage-for-perf-tests/mltransform/vocab_artifacts_${{env.NOW_UTC}}
--output_vocab=gs://temp-storage-for-perf-tests/mltransform/vocab_outputs/mltransform_generate_vocab_batch_${{env.NOW_UTC}}'
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_14 }}
--job_name=benchmark-tests-mltransform-generate-vocab-batch-${{env.NOW_UTC}}
--artifact_location=gs://temp-storage-for-perf-tests/mltransform/vocab_artifacts_${{env.NOW_UTC}}
--output_vocab=gs://temp-storage-for-perf-tests/mltransform/vocab_outputs/mltransform_generate_vocab_batch_${{env.NOW_UTC}}'
- name: run MLTransform One-Hot Encoding Batch
uses: ./.github/actions/gradle-command-self-hosted-action
timeout-minutes: 180
@@ -242,7 +377,7 @@ jobs:
-Prunner=DataflowRunner \
-PpythonVersion=3.10 \
-PloadTest.requirementsTxtFile=apache_beam/examples/ml_transform/mltransform_one_hot_encoding_requirements.txt
\
- '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_12 }}
--autoscaling_algorithm=NONE --metrics_table=mltransform_one_hot_encoding_batch
--influx_measurement=mltransform_one_hot_encoding_batch
--job_name=benchmark-tests-mltransform-one-hot-encoding-batch-${{env.NOW_UTC}}
--output_file=gs://temp-storage-for-end-to-end-tests/mltransform/one_hot_output_${{env.NOW_UTC}}
--artifact_location=gs://temp-storage-for-end-to-end-tests/mltransform/artifacts
[...]
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_15 }}
--autoscaling_algorithm=NONE --metrics_table=mltransform_one_hot_encoding_batch
--influx_measurement=mltransform_one_hot_encoding_batch
--job_name=benchmark-tests-mltransform-one-hot-encoding-batch-${{env.NOW_UTC}}
--output_file=gs://temp-storage-for-end-to-end-tests/mltransform/one_hot_output_${{env.NOW_UTC}}
--artifact_location=gs://temp-storage-for-end-to-end-tests/mltransform/artifacts
[...]
- name: run MLTransform Image Embedding GPU Batch
uses: ./.github/actions/gradle-command-self-hosted-action
timeout-minutes: 180
@@ -253,7 +388,7 @@ jobs:
-Prunner=DataflowRunner \
-PpythonVersion=3.10 \
-PloadTest.requirementsTxtFile=apache_beam/ml/transforms/mltransform_embedding_tests_requirements.txt
\
- '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_13 }}
--artifact_location=gs://temp-storage-for-perf-tests/loadtests/mltransform_image_embedding_gpu/artifacts-${{env.NOW_UTC}}
--job_name=benchmark-tests-mltransform-image-embedding-gpu-batch-${{env.NOW_UTC}}'
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_16 }}
--artifact_location=gs://temp-storage-for-perf-tests/loadtests/mltransform_image_embedding_gpu/artifacts-${{env.NOW_UTC}}
--job_name=benchmark-tests-mltransform-image-embedding-gpu-batch-${{env.NOW_UTC}}'
- name: run MLTransform Text Embedding Batch
uses: ./.github/actions/gradle-command-self-hosted-action
timeout-minutes: 180
@@ -264,7 +399,7 @@ jobs:
-Prunner=DataflowRunner \
-PpythonVersion=3.10 \
-PloadTest.requirementsTxtFile=apache_beam/ml/transforms/mltransform_embedding_tests_requirements.txt
\
- '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_14 }}
--output=gs://temp-storage-for-perf-tests/loadtests/mltransform_text_embedding/results-${{env.NOW_UTC}}
--artifact_location=gs://temp-storage-for-perf-tests/loadtests/mltransform_text_embedding/artifacts-${{env.NOW_UTC}}
--job_name=benchmark-tests-mltransform-text-embedding-batch-${{env.NOW_UTC}}'
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_17 }}
--output=gs://temp-storage-for-perf-tests/loadtests/mltransform_text_embedding/results-${{env.NOW_UTC}}
--artifact_location=gs://temp-storage-for-perf-tests/loadtests/mltransform_text_embedding/artifacts-${{env.NOW_UTC}}
--job_name=benchmark-tests-mltransform-text-embedding-batch-${{env.NOW_UTC}}'
- name: run MLTransform Image Embedding CPU Batch
uses: ./.github/actions/gradle-command-self-hosted-action
timeout-minutes: 180
@@ -275,4 +410,4 @@ jobs:
-Prunner=DataflowRunner \
-PpythonVersion=3.10 \
-PloadTest.requirementsTxtFile=apache_beam/ml/transforms/mltransform_embedding_tests_requirements.txt
\
- '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_15 }}
--artifact_location=gs://temp-storage-for-perf-tests/loadtests/mltransform_image_embedding_cpu/artifacts-${{env.NOW_UTC}}
--job_name=benchmark-tests-mltransform-image-embedding-cpu-batch-${{env.NOW_UTC}}'
+ '-PloadTest.args=${{
env.beam_Inference_Python_Benchmarks_Dataflow_test_arguments_18 }}
--artifact_location=gs://temp-storage-for-perf-tests/loadtests/mltransform_image_embedding_cpu/artifacts-${{env.NOW_UTC}}
--job_name=benchmark-tests-mltransform-image-embedding-cpu-batch-${{env.NOW_UTC}}'
diff --git
a/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Captioning.txt
b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Captioning.txt
new file mode 100644
index 00000000000..c58707383ef
--- /dev/null
+++
b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Captioning.txt
@@ -0,0 +1,37 @@
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+--region=us-central1
+--worker_machine_type=n1-standard-4
+--num_workers=50
+--disk_size_gb=50
+--autoscaling_algorithm=NONE
+--staging_location=gs://temp-storage-for-perf-tests/loadtests
+--temp_location=gs://temp-storage-for-perf-tests/loadtests
+--requirements_file=apache_beam/ml/inference/pytorch_image_captioning_requirements.txt
+--publish_to_big_query=true
+--metrics_dataset=beam_run_inference
+--input_options={}
+--influx_measurement=result_torch_inference_image_captioning
+--input=gs://apache-beam-ml/testing/inputs/openimage_50k_benchmark.txt
+--blip_model_name=Salesforce/blip-image-captioning-base
+--blip_batch_size=4
+--num_captions=5
+--max_new_tokens=30
+--num_beams=5
+--clip_model_name=openai/clip-vit-base-patch32
+--clip_batch_size=8
+--clip_score_normalize=false
+--runner=DataflowRunner
+--experiments=use_runner_v2
diff --git
a/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Classification_Rightfit.txt
b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Classification_Rightfit.txt
new file mode 100644
index 00000000000..1140ee5735e
--- /dev/null
+++
b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Classification_Rightfit.txt
@@ -0,0 +1,41 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+--region=us-central1
+--machine_type=n1-standard-4
+--num_workers=50
+--disk_size_gb=50
+--autoscaling_algorithm=NONE
+--staging_location=gs://temp-storage-for-perf-tests/loadtests
+--temp_location=gs://temp-storage-for-perf-tests/loadtests
+--requirements_file=apache_beam/ml/inference/pytorch_rightfit_requirements.txt
+--publish_to_big_query=true
+--metrics_dataset=beam_run_inference
+--influx_measurement=torch_inference_imagenet_stream_rightfit
+--pretrained_model_name=efficientnet_b0
+--input_file=gs://apache-beam-ml/testing/inputs/openimage_50k_benchmark.txt
+--runner=DataflowRunner
+--mode=streaming
+--input_mode=gcs_uris
+--input_options={}
+--pubsub_topic=projects/apache-beam-testing/topics/images_topic
+--pubsub_subscription=projects/apache-beam-testing/subscriptions/images_subscription
+--model_state_dict_path=gs://apache-beam-ml/models/efficientnet_b0_state_dict.pth
+--image_size=224
+--top_k=5
+--inference_batch_size=16
+--window_sec=60
+--trigger_proc_time_sec=30
diff --git
a/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Object_Detection.txt
b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Object_Detection.txt
new file mode 100644
index 00000000000..18f017ad010
--- /dev/null
+++
b/.github/workflows/load-tests-pipeline-options/beam_Inference_Python_Benchmarks_Dataflow_Pytorch_Image_Object_Detection.txt
@@ -0,0 +1,37 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+--region=us-central1
+--worker_machine_type=n1-standard-4
+--num_workers=50
+--disk_size_gb=50
+--autoscaling_algorithm=NONE
+--staging_location=gs://temp-storage-for-perf-tests/loadtests
+--temp_location=gs://temp-storage-for-perf-tests/loadtests
+--requirements_file=apache_beam/ml/inference/pytorch_image_object_detection_requirements.txt
+--publish_to_big_query=true
+--metrics_dataset=beam_run_inference
+--input_options={}
+--influx_measurement=result_torch_inference_image_object_detection_batch
+--pretrained_model_name=fasterrcnn_resnet50_fpn
+--inference_batch_size=8
+--resize_shorter_side=800
+--score_threshold=0.5
+--max_detections=50
+--input=gs://apache-beam-ml/testing/inputs/openimage_50k_benchmark.txt
+--model_state_dict_path=gs://apache-beam-ml/models/torchvision.detection.fasterrcnn_resnet50_fpn.pth
+--runner=DataflowRunner
+--experiments=use_runner_v2
diff --git a/.test-infra/tools/refresh_looker_metrics.py
b/.test-infra/tools/refresh_looker_metrics.py
index 69798e6f24e..351d8998f8f 100644
--- a/.test-infra/tools/refresh_looker_metrics.py
+++ b/.test-infra/tools/refresh_looker_metrics.py
@@ -43,6 +43,25 @@ LOOKS_TO_DOWNLOAD = [
("82", ["263", "264", "265", "266", "267"]), # PyTorch Sentiment
Streaming DistilBERT base uncased
("85", ["268", "269", "270", "271", "272"]), # PyTorch Sentiment Batch
DistilBERT base uncased
("86", ["284", "285", "286", "287", "288"]), # VLLM Batch Gemma
+
+ # PyTorch Image Classification EfficientNet-B0 Streaming (Right-fit) CPU
+ ("92", ["289", "290", "291", "292", "293"]),
+ ("97", ["306", "307", "308", "309", "310"]),
+ ("98", ["311", "312", "313", "314", "315"]),
+ ("99", ["316", "317", "318", "319", "320"]),
+
+ # PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch CPU
+ ("93", ["294", "295", "296", "298", "299"]),
+ ("100", ["321", "322", "323", "324", "325"]),
+ ("101", ["326", "327", "328", "329", "330"]),
+ ("102", ["331", "332", "333", "334", "335"]),
+
+ # PyTorch Image Captioning BLIP + CLIP Batch CPU
+ ("94", ["297", "300", "301", "302", "303"]),
+ ("103", ["336", "337", "338", "339", "340"]),
+ ("104", ["341", "342", "343", "344", "345"]),
+ ("105", ["346", "347", "348", "349", "350"]),
+
("96", ["270", "304", "305", "353", "354"]), # Table Row Inference
Sklearn Batch
("106", ["355", "356", "357", "358", "359"]), # Table Row Inference
Sklearn Streaming
("107", ["360", "361", "362", "363", "364"]), # MLTransform Generate
Vocab Batch
diff --git
a/sdks/python/apache_beam/examples/inference/pytorch_image_captioning.py
b/sdks/python/apache_beam/examples/inference/pytorch_image_captioning.py
new file mode 100644
index 00000000000..eb9fa903e61
--- /dev/null
+++ b/sdks/python/apache_beam/examples/inference/pytorch_image_captioning.py
@@ -0,0 +1,641 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""This pipeline performs image captioning using a multi-model approach:
+BLIP generates candidate captions, CLIP ranks them by image-text similarity.
+
+The pipeline reads image URIs from a GCS input file, decodes images, runs BLIP
+caption generation in batches on GPU, then runs CLIP ranking in batches on GPU.
+Results are written to BigQuery.
+"""
+
+import argparse
+import io
+import json
+import logging
+import threading
+import time
+from typing import Any
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import Tuple
+
+import apache_beam as beam
+from apache_beam.io.filesystems import FileSystems
+from apache_beam.ml.inference.base import KeyedModelHandler
+from apache_beam.ml.inference.base import ModelHandler
+from apache_beam.ml.inference.base import PredictionResult
+from apache_beam.ml.inference.base import RunInference
+from apache_beam.options.pipeline_options import PipelineOptions
+from apache_beam.options.pipeline_options import SetupOptions
+from apache_beam.options.pipeline_options import StandardOptions
+from apache_beam.runners.runner import PipelineResult
+from apache_beam.transforms import window
+
+from google.api_core.exceptions import NotFound
+from google.cloud import pubsub_v1
+import torch
+import PIL.Image as PILImage
+
+# ============ Utility ============
+
+
+def now_millis() -> int:
+ return int(time.time() * 1000)
+
+
+def decode_pil(image_bytes: bytes) -> PILImage.Image:
+ with PILImage.open(io.BytesIO(image_bytes)) as img:
+ img = img.convert("RGB")
+ img.load()
+ return img
+
+
+# ============ DoFns ============
+
+
+class MakeKeyDoFn(beam.DoFn):
+ """Produce (uri, uri) so the URI is used as the stable key."""
+ def process(self, element: str):
+ uri = element
+ yield uri, uri
+
+
+class ReadImageBytesDoFn(beam.DoFn):
+ """Turn (uri, uri) -> (uri, dict(image_bytes))."""
+ def process(self, kv: Tuple[str, str]):
+ uri, _ = kv
+ try:
+ with FileSystems.open(uri) as f:
+ image_bytes = f.read()
+ yield uri, {"image_bytes": image_bytes}
+ except OSError as e:
+ logging.warning("Failed to read image %s: %s", uri, e)
+ return
+
+
+class DecodeImageDoFn(beam.DoFn):
+ """Turn (uri, dict(image_bytes)) -> (uri, dict(image))."""
+ def process(self, kv: Tuple[str, Dict[str, Any]]):
+ uri, value = kv
+ image_bytes = value["image_bytes"]
+
+ try:
+ image = decode_pil(image_bytes)
+ except (OSError, ValueError) as e:
+ logging.warning("Failed to decode image %s: %s", uri, e)
+ image = PILImage.new("RGB", (224, 224), color=(0, 0, 0))
+
+ yield uri, {"image": image}
+
+
+class PostProcessDoFn(beam.DoFn):
+ """Final PredictionResult -> row for BigQuery."""
+ def __init__(self, blip_name: str, clip_name: str):
+ self.blip_name = blip_name
+ self.clip_name = clip_name
+
+ def process(self, kv: Tuple[str, PredictionResult]):
+ uri, pred = kv
+ if hasattr(pred, "inference"):
+ inf = pred.inference or {}
+ else:
+ inf = pred
+ # Expected inference fields from CLIP handler:
+ # best_caption, best_score, candidates, scores, blip_ms, clip_ms, total_ms
+ best_caption = inf.get("best_caption", "")
+ best_score = inf.get("best_score", None)
+ candidates = inf.get("candidates", [])
+ scores = inf.get("scores", [])
+ blip_ms = inf.get("blip_ms", None)
+ clip_ms = inf.get("clip_ms", None)
+ total_ms = inf.get("total_ms", None)
+
+ yield {
+ "image_id": uri,
+ "blip_model": self.blip_name,
+ "clip_model": self.clip_name,
+ "best_caption": best_caption,
+ "best_score": float(best_score) if best_score is not None else None,
+ "candidates": json.dumps(candidates),
+ "scores": json.dumps(scores),
+ "blip_ms": int(blip_ms) if blip_ms is not None else None,
+ "clip_ms": int(clip_ms) if clip_ms is not None else None,
+ "total_ms": int(total_ms) if total_ms is not None else None,
+ "infer_ms": now_millis(),
+ }
+
+
+# ============ Model Handlers ============
+
+
+class BlipCaptionModelHandler(ModelHandler):
+ def __init__(
+ self,
+ model_name: str,
+ device: str,
+ batch_size: int,
+ num_captions: int,
+ max_new_tokens: int,
+ num_beams: int):
+ self.model_name = model_name
+ self.device = device
+ self.batch_size = batch_size
+ self.num_captions = num_captions
+ self.max_new_tokens = max_new_tokens
+ self.num_beams = num_beams
+
+ def load_model(self):
+ from transformers import BlipForConditionalGeneration, BlipProcessor
+ processor = BlipProcessor.from_pretrained(self.model_name)
+ model = BlipForConditionalGeneration.from_pretrained(self.model_name)
+ model.to(self.device)
+ model.eval()
+ return (model, processor)
+
+ def batch_elements_kwargs(self):
+ return {"max_batch_size": self.batch_size}
+
+ def run_inference(
+ self, batch: List[Dict[str, Any]], model_bundle, inference_args=None):
+
+ model, processor = model_bundle
+ start = now_millis()
+
+ images = [x["image"] for x in batch]
+
+ # Processor makes pixel_values
+ inputs = processor(images=images, return_tensors="pt")
+ pixel_values = inputs["pixel_values"].to(self.device)
+
+ # Generate captions
+ # We use num_return_sequences to generate multiple candidates per image.
+ # Note: this will produce (B * num_captions) sequences.
+ with torch.no_grad():
+ generated_ids = model.generate(
+ pixel_values=pixel_values,
+ max_new_tokens=self.max_new_tokens,
+ num_beams=max(self.num_beams, self.num_captions),
+ num_return_sequences=self.num_captions,
+ do_sample=False,
+ )
+
+ captions_all = processor.batch_decode(
+ generated_ids, skip_special_tokens=True)
+
+ # Group candidates per image
+ candidates_per_image = []
+ idx = 0
+ for _ in range(len(batch)):
+ candidates_per_image.append(captions_all[idx:idx + self.num_captions])
+ idx += self.num_captions
+
+ blip_ms = now_millis() - start
+
+ results = []
+ for i in range(len(batch)):
+ results.append({
+ "image": images[i],
+ "candidates": candidates_per_image[i],
+ "blip_ms": blip_ms,
+ })
+ return results
+
+ def get_metrics_namespace(self) -> str:
+ return "blip_captioning"
+
+
+class ClipRankModelHandler(ModelHandler):
+ def __init__(
+ self,
+ model_name: str,
+ device: str,
+ batch_size: int,
+ score_normalize: bool):
+ self.model_name = model_name
+ self.device = device
+ self.batch_size = batch_size
+ self.score_normalize = score_normalize
+
+ def load_model(self):
+ from transformers import CLIPModel, CLIPProcessor
+ processor = CLIPProcessor.from_pretrained(self.model_name)
+ model = CLIPModel.from_pretrained(self.model_name)
+ model.to(self.device)
+ model.eval()
+ return (model, processor)
+
+ def batch_elements_kwargs(self):
+ return {"max_batch_size": self.batch_size}
+
+ def run_inference(
+ self, batch: List[Dict[str, Any]], model_bundle, inference_args=None):
+
+ model, processor = model_bundle
+ start_batch = now_millis()
+
+ # Flat lists for a single batched CLIP forward pass
+ images: List[PILImage.Image] = []
+ texts: List[str] = []
+ offsets: List[Tuple[int, int, int]] = []
+ candidates_list: List[List[str]] = []
+ blip_ms_list: List[Optional[int]] = []
+
+ for x in batch:
+ img = x["image"]
+ candidates = [str(c) for c in (x.get("candidates", []) or [])]
+ candidates_list.append(candidates)
+ blip_ms_list.append(x.get("blip_ms", None))
+
+ image_idx = len(images)
+ images.append(img)
+
+ start_i = len(texts)
+ texts.extend(candidates)
+ end_i = len(texts)
+ offsets.append((image_idx, start_i, end_i))
+
+ results: List[Dict[str, Any]] = []
+
+ # Fast path: no candidates at all
+ if not texts:
+ for blip_ms in blip_ms_list:
+ total_ms = int(blip_ms) if blip_ms is not None else None
+ results.append({
+ "best_caption": "",
+ "best_score": None,
+ "candidates": [],
+ "scores": [],
+ "blip_ms": blip_ms,
+ "clip_ms": 0,
+ "total_ms": total_ms,
+ })
+ return results
+
+ with torch.no_grad():
+ image_inputs = processor(
+ images=images,
+ return_tensors="pt",
+ ).to(self.device)
+
+ text_inputs = processor(
+ text=texts,
+ return_tensors="pt",
+ padding=True,
+ truncation=True,
+ ).to(self.device)
+
+ image_features = model.get_image_features(
+ pixel_values=image_inputs["pixel_values"])
+ text_features = model.get_text_features(
+ input_ids=text_inputs["input_ids"],
+ attention_mask=text_inputs.get("attention_mask"),
+ )
+
+ image_features = image_features / image_features.norm(
+ dim=-1, keepdim=True)
+ text_features = text_features / text_features.norm(dim=-1, keepdim=True)
+
+ logit_scale = model.logit_scale.exp()
+
+ batch_ms = now_millis() - start_batch
+ total_pairs = len(texts)
+
+ items = zip(offsets, candidates_list, blip_ms_list)
+ for (image_idx, start_i, end_i), candidates, blip_ms in items:
+ if start_i == end_i:
+ total_ms = int(blip_ms) if blip_ms is not None else None
+ results.append({
+ "best_caption": "",
+ "best_score": None,
+ "candidates": [],
+ "scores": [],
+ "blip_ms": blip_ms,
+ "clip_ms": 0,
+ "total_ms": total_ms,
+ })
+ continue
+
+ candidate_features = text_features[start_i:end_i]
+ image_feature = image_features[image_idx].unsqueeze(0)
+
+ pair_scores = (candidate_features *
+ image_feature).sum(dim=-1) * logit_scale
+
+ scores = pair_scores.detach().cpu().tolist()
+
+ if self.score_normalize:
+ scores_t = torch.tensor(scores, dtype=torch.float32)
+ scores = torch.softmax(scores_t, dim=0).tolist()
+
+ best_idx = max(range(len(scores)), key=lambda i, s=scores: s[i])
+
+ pairs = end_i - start_i
+ clip_ms_elem = int(batch_ms * (pairs / max(1, total_pairs)))
+ if pairs > 0:
+ clip_ms_elem = max(1, clip_ms_elem)
+
+ total_ms = int(blip_ms) + clip_ms_elem if blip_ms is not None else None
+ results.append({
+ "best_caption": candidates[best_idx],
+ "best_score": float(scores[best_idx]),
+ "candidates": candidates,
+ "scores": scores,
+ "blip_ms": blip_ms,
+ "clip_ms": clip_ms_elem,
+ "total_ms": total_ms,
+ })
+
+ return results
+
+ def get_metrics_namespace(self) -> str:
+ return "clip_ranking"
+
+
+# ============ Args & Helpers ============
+
+
+def parse_known_args(argv):
+ parser = argparse.ArgumentParser()
+
+ # I/O & runtime
+ parser.add_argument(
+ '--mode', default='streaming', choices=['streaming', 'batch'])
+ parser.add_argument(
+ '--project', default='apache-beam-testing', help='GCP project ID')
+ parser.add_argument(
+ '--input', required=True, help='GCS path to file with image URIs')
+ parser.add_argument(
+ '--pubsub_topic',
+ default='projects/apache-beam-testing/topics/images_topic')
+ parser.add_argument(
+ '--pubsub_subscription',
+ default='projects/apache-beam-testing/subscriptions/images_subscription')
+ parser.add_argument(
+ '--output_table',
+ required=True,
+ help='BigQuery output table: dataset.table')
+ parser.add_argument(
+ '--publish_to_big_query', default='true', choices=['true', 'false'])
+ parser.add_argument(
+ '--feeder_start_delay_sec',
+ type=int,
+ default=900,
+ help=(
+ 'Delay before starting the feeder pipeline that reads URIs from GCS '
+ 'and publishes them to Pub/Sub. This delay allows the main streaming
'
+ 'pipeline workers to start and scale before data ingestion begins.'),
+ )
+
+ # Device
+ parser.add_argument('--device', default='GPU', choices=['CPU', 'GPU'])
+
+ # BLIP
+ parser.add_argument(
+ '--blip_model_name', default='Salesforce/blip-image-captioning-base')
+ parser.add_argument('--blip_batch_size', type=int, default=4)
+ parser.add_argument('--num_captions', type=int, default=5)
+ parser.add_argument('--max_new_tokens', type=int, default=30)
+ parser.add_argument('--num_beams', type=int, default=5)
+
+ # CLIP
+ parser.add_argument(
+ '--clip_model_name', default='openai/clip-vit-base-patch32')
+ parser.add_argument('--clip_batch_size', type=int, default=8)
+ parser.add_argument(
+ '--clip_score_normalize', default='false', choices=['true', 'false'])
+
+ # Windows
+ parser.add_argument('--window_sec', type=int, default=60)
+ parser.add_argument('--trigger_proc_time_sec', type=int, default=30)
+
+ known_args, pipeline_args = parser.parse_known_args(argv)
+ return known_args, pipeline_args
+
+
+def ensure_pubsub_resources(
+ project: str, topic_path: str, subscription_path: str):
+ publisher = pubsub_v1.PublisherClient()
+ subscriber = pubsub_v1.SubscriberClient()
+
+ try:
+ publisher.get_topic(request={"topic": topic_path})
+ except NotFound:
+ publisher.create_topic(name=topic_path)
+
+ try:
+ subscriber.get_subscription(request={"subscription": subscription_path})
+ except NotFound:
+ subscriber.create_subscription(name=subscription_path, topic=topic_path)
+
+
+def cleanup_pubsub_resources(
+ project: str, topic_path: str, subscription_path: str):
+ publisher = pubsub_v1.PublisherClient()
+ subscriber = pubsub_v1.SubscriberClient()
+
+ try:
+ subscriber.delete_subscription(request={"subscription": subscription_path})
+ logging.info(f"Deleted subscription: {subscription_path}")
+ except NotFound:
+ logging.info(f"Subscription already deleted: {subscription_path}")
+
+ try:
+ publisher.delete_topic(request={"topic": topic_path})
+ logging.info(f"Deleted topic: {topic_path}")
+ except NotFound:
+ logging.info(f"Topic already deleted: {topic_path}")
+
+ try:
+ subscriber.delete_subscription(
+ request={"subscription": full_subscription_path})
+ logging.info(f"Deleted subscription: {subscription_name}")
+ except NotFound:
+ logging.info(f"Subscription already deleted: {subscription_name}")
+
+ try:
+ publisher.delete_topic(request={"topic": full_topic_path})
+ logging.info(f"Deleted topic: {topic_name}")
+ except NotFound:
+ logging.info(f"Topic already deleted: {topic_name}")
+
+
+def override_or_add(args, flag, value):
+ if flag in args:
+ idx = args.index(flag)
+ args[idx + 1] = str(value)
+ else:
+ args.extend([flag, str(value)])
+
+
+# ============ Load pipeline ============
+
+
+def run_load_pipeline(known_args, pipeline_args):
+ """Reads GCS file with URIs and publishes them to Pub/Sub (for streaming)."""
+
+ pipeline_args = list(pipeline_args)
+ # enforce smaller/CPU-only defaults for feeder
+ override_or_add(pipeline_args, '--device', 'CPU')
+ override_or_add(pipeline_args, '--num_workers', '5')
+ override_or_add(pipeline_args, '--max_num_workers', '10')
+ override_or_add(
+ pipeline_args, '--job_name', f"images-load-pubsub-{int(time.time())}")
+ override_or_add(pipeline_args, '--project', known_args.project)
+ pipeline_args = [
+ arg for arg in pipeline_args if not arg.startswith("--experiments")
+ ]
+
+ pipeline_options = PipelineOptions(pipeline_args)
+ pipeline = beam.Pipeline(options=pipeline_options)
+
+ _ = (
+ pipeline
+ | 'ReadGCSFile' >> beam.io.ReadFromText(known_args.input)
+ | 'FilterEmpty' >> beam.Filter(lambda line: line.strip())
+ | 'ToBytes' >> beam.Map(lambda line: line.encode('utf-8'))
+ | 'ToPubSub' >> beam.io.WriteToPubSub(topic=known_args.pubsub_topic))
+ return pipeline.run()
+
+
+# ============ Main pipeline ============
+
+
+def run(
+ argv=None, save_main_session=True, test_pipeline=None) -> PipelineResult:
+ known_args, pipeline_args = parse_known_args(argv)
+
+ if known_args.mode == 'streaming':
+ ensure_pubsub_resources(
+ project=known_args.project,
+ topic_path=known_args.pubsub_topic,
+ subscription_path=known_args.pubsub_subscription)
+
+ # Start feeder thread that reads URIs from GCS and fills Pub/Sub.
+ # Delay is used to allow the main streaming pipeline workers to start
+ # and autoscale before the feeder pipeline begins publishing messages.
+ threading.Thread(
+ target=lambda: (
+ time.sleep(known_args.feeder_start_delay_sec), run_load_pipeline(
+ known_args, pipeline_args)),
+ daemon=True).start()
+
+ pipeline_options = PipelineOptions(pipeline_args)
+ pipeline_options.view_as(SetupOptions).save_main_session = save_main_session
+ pipeline_options.view_as(StandardOptions).streaming = (
+ known_args.mode == 'streaming')
+
+ device = 'cuda' if known_args.device.upper() == 'GPU' else 'cpu'
+ clip_score_normalize = (known_args.clip_score_normalize == 'true')
+
+ blip_handler = BlipCaptionModelHandler(
+ model_name=known_args.blip_model_name,
+ device=device,
+ batch_size=int(known_args.blip_batch_size),
+ num_captions=int(known_args.num_captions),
+ max_new_tokens=int(known_args.max_new_tokens),
+ num_beams=int(known_args.num_beams),
+ )
+
+ clip_handler = ClipRankModelHandler(
+ model_name=known_args.clip_model_name,
+ device=device,
+ batch_size=int(known_args.clip_batch_size),
+ score_normalize=clip_score_normalize,
+ )
+
+ pipeline = test_pipeline or beam.Pipeline(options=pipeline_options)
+
+ if known_args.mode == 'batch':
+ pcoll = (
+ pipeline
+ | 'ReadURIsBatch' >> beam.io.ReadFromText(known_args.input)
+ | 'FilterEmptyBatch' >> beam.Filter(lambda s: s.strip()))
+ else:
+ pcoll = (
+ pipeline
+ | 'ReadFromPubSub' >>
+ beam.io.ReadFromPubSub(subscription=known_args.pubsub_subscription)
+ | 'DecodeUTF8' >> beam.Map(lambda x: x.decode('utf-8'))
+ | 'Window' >> beam.WindowInto(
+ window.FixedWindows(known_args.window_sec),
+ trigger=beam.trigger.AfterProcessingTime(
+ known_args.trigger_proc_time_sec),
+ accumulation_mode=beam.trigger.AccumulationMode.DISCARDING,
+ allowed_lateness=0))
+
+ keyed = (pcoll | 'MakeKey' >> beam.ParDo(MakeKeyDoFn()))
+ image_bytes = (keyed | 'ReadImageBytes' >> beam.ParDo(ReadImageBytesDoFn()))
+ images = (image_bytes | 'DecodeImage' >> beam.ParDo(DecodeImageDoFn()))
+
+ # Stage 1: BLIP candidate generation
+ blip_out = (
+ images
+ | 'RunInferenceBLIP' >> RunInference(KeyedModelHandler(blip_handler)))
+
+ # Stage 2: CLIP ranking over candidates
+ clip_out = (
+ blip_out
+ | 'RunInferenceCLIP' >> RunInference(KeyedModelHandler(clip_handler)))
+
+ results = (
+ clip_out
+ | 'PostProcess' >> beam.ParDo(
+ PostProcessDoFn(
+ blip_name=known_args.blip_model_name,
+ clip_name=known_args.clip_model_name)))
+
+ method = (
+ beam.io.WriteToBigQuery.Method.FILE_LOADS if known_args.mode == 'batch'
+ else beam.io.WriteToBigQuery.Method.STREAMING_INSERTS)
+
+ if known_args.publish_to_big_query == 'true':
+ _ = (
+ results
+ | 'WriteToBigQuery' >> beam.io.WriteToBigQuery(
+ known_args.output_table,
+ schema=(
+ 'image_id:STRING, blip_model:STRING, clip_model:STRING, '
+ 'best_caption:STRING, best_score:FLOAT, '
+ 'candidates:STRING, scores:STRING, '
+ 'blip_ms:INT64, clip_ms:INT64, total_ms:INT64,
infer_ms:INT64'),
+ write_disposition=beam.io.BigQueryDisposition.WRITE_APPEND,
+ create_disposition=beam.io.BigQueryDisposition.CREATE_IF_NEEDED,
+ method=method))
+
+ result = pipeline.run()
+ try:
+ result.wait_until_finish(duration=1800000) # 30 min
+ finally:
+ try:
+ result.cancel()
+ except Exception:
+ logging.debug("Failed to cancel pipeline result.", exc_info=True)
+
+ if known_args.mode == 'streaming':
+ cleanup_pubsub_resources(
+ project=known_args.project,
+ topic_path=known_args.pubsub_topic,
+ subscription_path=known_args.pubsub_subscription)
+
+ return result
+
+
+if __name__ == '__main__':
+ logging.getLogger().setLevel(logging.INFO)
+ run()
diff --git
a/sdks/python/apache_beam/examples/inference/pytorch_image_object_detection.py
b/sdks/python/apache_beam/examples/inference/pytorch_image_object_detection.py
new file mode 100644
index 00000000000..a8eef5a7d5e
--- /dev/null
+++
b/sdks/python/apache_beam/examples/inference/pytorch_image_object_detection.py
@@ -0,0 +1,517 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""This pipeline performs object detection using an open-source PyTorch
+TorchVision detection model (e.g., Faster R-CNN ResNet50 FPN) on GPU.
+
+It reads image URIs from a GCS input file, decodes and preprocesses images,
+runs batched GPU inference via RunInference, post-processes detection outputs,
+and writes results to BigQuery.
+
+The pipeline targets stable and reproducible performance measurements for
+GPU inference workloads (no right-fitting; fixed batch size).
+"""
+
+import argparse
+import io
+import json
+import logging
+import threading
+import time
+from typing import Any
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+
+import apache_beam as beam
+from apache_beam.io.filesystems import FileSystems
+from apache_beam.ml.inference.base import KeyedModelHandler
+from apache_beam.ml.inference.base import PredictionResult
+from apache_beam.ml.inference.base import RunInference
+from apache_beam.ml.inference.pytorch_inference import
PytorchModelHandlerTensor
+from apache_beam.options.pipeline_options import PipelineOptions
+from apache_beam.options.pipeline_options import SetupOptions
+from apache_beam.options.pipeline_options import StandardOptions
+from apache_beam.runners.runner import PipelineResult
+from apache_beam.transforms import window
+
+from google.api_core.exceptions import NotFound
+from google.cloud import pubsub_v1
+import torch
+import PIL.Image as PILImage
+
+# ============ Utility & Preprocessing ============
+
+
+def now_millis() -> int:
+ return int(time.time() * 1000)
+
+
+def decode_to_tens(image_bytes: bytes, image_size: int = 800) -> torch.Tensor:
+ """Decode bytes -> RGB PIL -> resize/pad square -> float tensor [0..1], CHW.
+
+ TorchVision detection models accept float tensors in [0..1]. We force a fixed
+ square shape so PytorchModelHandlerTensor can batch tensors with torch.stack.
+ """
+ with PILImage.open(io.BytesIO(image_bytes)) as img:
+ img = img.convert("RGB")
+
+ w, h = img.size
+ scale = min(image_size / float(w), image_size / float(h))
+ new_w = max(1, int(round(w * scale)))
+ new_h = max(1, int(round(h * scale)))
+
+ img = img.resize((new_w, new_h))
+
+ padded = PILImage.new("RGB", (image_size, image_size), color=(0, 0, 0))
+ left = (image_size - new_w) // 2
+ top = (image_size - new_h) // 2
+ padded.paste(img, (left, top))
+
+ import numpy as np
+ arr = np.asarray(padded).astype("float32") / 255.0
+ arr = np.transpose(arr, (2, 0, 1))
+ return torch.from_numpy(arr).float()
+
+
+# ============ DoFns ============
+
+
+class MakeKeyDoFn(beam.DoFn):
+ """Produce (uri, uri) where the URI is used as the stable key."""
+ def process(self, element: str):
+ uri = element
+ yield uri, uri
+
+
+class DecodePreprocessDoFn(beam.DoFn):
+ """Turn (uri, uri) -> (uri, tensor)."""
+ def __init__(self, image_size: int = 800):
+ self.image_size = image_size
+
+ def process(self, kv: Tuple[str, str]):
+ uri, _ = kv
+ start = now_millis()
+ try:
+ with FileSystems.open(uri) as f:
+ image_bytes = f.read()
+ tensor = decode_to_tens(image_bytes, image_size=self.image_size)
+ preprocess_ms = now_millis() - start
+ yield uri, {"tensor": tensor, "preprocess_ms": preprocess_ms}
+ except (OSError, ValueError):
+ logging.exception("Decode failed for %s", uri)
+ return
+
+
+# pylint: disable=unused-argument
+def _torchvision_detection_inference_fn(
+ batch: Sequence[torch.Tensor],
+ model: torch.nn.Module,
+ device: torch.device,
+ inference_args: Optional[dict[str, Any]] = None,
+ model_id: Optional[str] = None,
+) -> List[Dict[str, Any]]:
+ """Inference function for TorchVision detection models.
+
+ TorchVision detection models expect List[Tensor] where each tensor is:
+ - shape: [3, H, W]
+ - dtype: float32
+ - values: [0..1]
+ """
+
+ with torch.no_grad():
+ # Ensure tensors are on device
+ inputs = []
+ for t in batch:
+ if isinstance(t, torch.Tensor):
+ inputs.append(t.to(device))
+ else:
+ # Defensive: if somehow non-tensor slips through.
+ inputs.append(torch.as_tensor(t).to(device))
+ outputs = model(inputs) # List[Dict[str, Tensor]]
+ return outputs
+
+
+class PostProcessDoFn(beam.DoFn):
+ """PredictionResult -> dict row for BQ."""
+ def __init__(
+ self, model_name: str, score_threshold: float, max_detections: int):
+ self.model_name = model_name
+ self.score_threshold = score_threshold
+ self.max_detections = max_detections
+
+ def _extract_detection(self, inference_obj: Any) -> Dict[str, Any]:
+ """Extract detection fields from torchvision output dict."""
+ # Expect: {'boxes': Tensor[N,4], 'labels': Tensor[N], 'scores': Tensor[N]}
+ boxes = inference_obj.get("boxes")
+ labels = inference_obj.get("labels")
+ scores = inference_obj.get("scores")
+
+ # Convert to CPU lists
+ if isinstance(scores, torch.Tensor):
+ scores_list = scores.detach().cpu().tolist()
+ else:
+ scores_list = list(scores) if scores is not None else []
+
+ if isinstance(labels, torch.Tensor):
+ labels_list = labels.detach().cpu().tolist()
+ else:
+ labels_list = list(labels) if labels is not None else []
+
+ if isinstance(boxes, torch.Tensor):
+ boxes_list = boxes.detach().cpu().tolist()
+ else:
+ boxes_list = list(boxes) if boxes is not None else []
+
+ # Filter by threshold and trim to max_detections
+ dets = []
+ for i in range(min(len(scores_list), len(labels_list), len(boxes_list))):
+ score = float(scores_list[i])
+ if score < self.score_threshold:
+ continue
+ box = boxes_list[i] # [x1,y1,x2,y2]
+ dets.append({
+ "label_id": int(labels_list[i]),
+ "score": score,
+ "box": [float(box[0]), float(box[1]), float(box[2]), float(box[3])],
+ })
+ if len(dets) >= self.max_detections:
+ break
+
+ return {
+ "detections": dets,
+ "num_detections": len(dets),
+ }
+
+ def process(self, kv: Tuple[str, PredictionResult]):
+ image_uri, pred = kv
+
+ # pred can be PredictionResult OR raw torchvision dict.
+ if hasattr(pred, "inference"):
+ inference_obj = pred.inference
+ else:
+ inference_obj = pred
+
+ if isinstance(inference_obj, list) and len(inference_obj) == 1:
+ inference_obj = inference_obj[0]
+
+ if not isinstance(inference_obj, dict):
+ logging.warning(
+ "Unexpected inf-ce type for %s: %s", image_uri, type(inference_obj))
+ yield {
+ "image_id": image_uri,
+ "model_name": self.model_name,
+ "detections": json.dumps([]),
+ "num_detections": 0,
+ "infer_ms": now_millis(),
+ }
+ return
+
+ extracted = self._extract_detection(inference_obj)
+
+ yield {
+ "image_id": image_uri,
+ "model_name": self.model_name,
+ "detections": json.dumps(extracted["detections"]),
+ "num_detections": int(extracted["num_detections"]),
+ "infer_ms": now_millis(),
+ }
+
+
+# ============ Args & Helpers ============
+
+
+def parse_known_args(argv):
+ parser = argparse.ArgumentParser()
+
+ # I/O & runtime
+ parser.add_argument(
+ '--project', default='apache-beam-testing', help='GCP project ID')
+ parser.add_argument(
+ '--mode', default='streaming', choices=['streaming', 'batch'])
+ parser.add_argument(
+ '--output_table',
+ required=True,
+ help='BigQuery output table: dataset.table')
+ parser.add_argument(
+ '--publish_to_big_query', default='true', choices=['true', 'false'])
+ parser.add_argument(
+ '--input', required=True, help='GCS path to file with image URIs')
+ parser.add_argument(
+ '--pubsub_topic',
+ default='projects/apache-beam-testing/topics/images_topic')
+ parser.add_argument(
+ '--pubsub_subscription',
+ default='projects/apache-beam-testing/subscriptions/images_subscription')
+ parser.add_argument(
+ '--feeder_start_delay_sec',
+ type=int,
+ default=900,
+ help=(
+ 'Delay before starting the feeder pipeline that reads URIs from GCS '
+ 'and publishes them to Pub/Sub. This delay allows the main streaming
'
+ 'pipeline workers to start and scale before data ingestion begins.'),
+ )
+
+ # Model & inference
+ parser.add_argument(
+ '--pretrained_model_name',
+ default='fasterrcnn_resnet50_fpn',
+ help=(
+ 'TorchVision detection model name '
+ '(e.g., fasterrcnn_resnet50_fpn)'))
+ parser.add_argument(
+ '--model_state_dict_path',
+ required=True,
+ help='GCS path to a state_dict .pth for the chosen model')
+ parser.add_argument('--device', default='GPU', choices=['CPU', 'GPU'])
+
+ # Batch sizing (no right-fitting)
+ parser.add_argument('--inference_batch_size', type=int, default=8)
+
+ # Preprocess
+ parser.add_argument('--image_size', type=int, default=800)
+
+ # Postprocess
+ parser.add_argument('--score_threshold', type=float, default=0.5)
+ parser.add_argument('--max_detections', type=int, default=50)
+
+ # Windows
+ parser.add_argument('--window_sec', type=int, default=60)
+ parser.add_argument('--trigger_proc_time_sec', type=int, default=30)
+
+ known_args, pipeline_args = parser.parse_known_args(argv)
+ return known_args, pipeline_args
+
+
+def ensure_pubsub_resources(
+ project: str, topic_path: str, subscription_path: str):
+ publisher = pubsub_v1.PublisherClient()
+ subscriber = pubsub_v1.SubscriberClient()
+
+ try:
+ publisher.get_topic(request={"topic": topic_path})
+ except NotFound:
+ publisher.create_topic(name=topic_path)
+
+ try:
+ subscriber.get_subscription(request={"subscription": subscription_path})
+ except NotFound:
+ subscriber.create_subscription(name=subscription_path, topic=topic_path)
+
+
+def cleanup_pubsub_resources(
+ project: str, topic_path: str, subscription_path: str):
+ publisher = pubsub_v1.PublisherClient()
+ subscriber = pubsub_v1.SubscriberClient()
+
+ try:
+ subscriber.delete_subscription(request={"subscription": subscription_path})
+ logging.info(f"Deleted subscription: {subscription_path}")
+ except NotFound:
+ logging.info(f"Subscription already deleted: {subscription_path}")
+
+ try:
+ publisher.delete_topic(request={"topic": topic_path})
+ logging.info(f"Deleted topic: {topic_path}")
+ except NotFound:
+ logging.info(f"Topic already deleted: {topic_path}")
+
+
+def override_or_add(args, flag, value):
+ if flag in args:
+ idx = args.index(flag)
+ args[idx + 1] = str(value)
+ else:
+ args.extend([flag, str(value)])
+
+
+def create_torchvision_detection_model(model_name: str):
+ """Creates a TorchVision detection model instance.
+
+ Note: We will load weights via state_dict_path (required by Beam handler when
+ model_class is provided).
+ """
+ import torchvision
+
+ name = model_name.strip()
+
+ if name == "fasterrcnn_resnet50_fpn":
+ model = torchvision.models.detection.fasterrcnn_resnet50_fpn(weights=None)
+ elif name == "retinanet_resnet50_fpn":
+ model = torchvision.models.detection.retinanet_resnet50_fpn(weights=None)
+ else:
+ raise ValueError(f"Unsupported detection model: {model_name}")
+
+ model.eval()
+ return model
+
+
+# ============ Load pipeline ============
+
+
+def run_load_pipeline(known_args, pipeline_args):
+ """Reads GCS file with URIs and publishes them to Pub/Sub (for streaming)."""
+
+ pipeline_args = list(pipeline_args)
+ # enforce smaller/CPU-only defaults for feeder
+ override_or_add(pipeline_args, '--device', 'CPU')
+ override_or_add(pipeline_args, '--num_workers', '5')
+ override_or_add(pipeline_args, '--max_num_workers', '10')
+ override_or_add(
+ pipeline_args, '--job_name', f"images-load-pubsub-{int(time.time())}")
+ override_or_add(pipeline_args, '--project', known_args.project)
+ pipeline_args = [
+ arg for arg in pipeline_args if not arg.startswith("--experiments")
+ ]
+
+ pipeline_options = PipelineOptions(pipeline_args)
+ pipeline = beam.Pipeline(options=pipeline_options)
+
+ _ = (
+ pipeline
+ | 'ReadGCSFile' >> beam.io.ReadFromText(known_args.input)
+ | 'FilterEmpty' >> beam.Filter(lambda line: line.strip())
+ | 'ToBytes' >> beam.Map(lambda line: line.encode('utf-8'))
+ | 'ToPubSub' >> beam.io.WriteToPubSub(topic=known_args.pubsub_topic))
+ return pipeline.run()
+
+
+# ============ Main pipeline ============
+
+
+def run(
+ argv=None, save_main_session=True, test_pipeline=None) -> PipelineResult:
+ known_args, pipeline_args = parse_known_args(argv)
+
+ if known_args.mode == 'streaming':
+ ensure_pubsub_resources(
+ project=known_args.project,
+ topic_path=known_args.pubsub_topic,
+ subscription_path=known_args.pubsub_subscription)
+
+ # Start feeder thread that reads URIs from GCS and fills Pub/Sub.
+ # Delay is used to allow the main streaming pipeline workers to start
+ # and autoscale before the feeder pipeline begins publishing messages.
+ threading.Thread(
+ target=lambda: (
+ time.sleep(known_args.feeder_start_delay_sec), run_load_pipeline(
+ known_args, pipeline_args)),
+ daemon=True).start()
+
+ pipeline_options = PipelineOptions(pipeline_args)
+ pipeline_options.view_as(SetupOptions).save_main_session = save_main_session
+ pipeline_options.view_as(StandardOptions).streaming = (
+ known_args.mode == 'streaming')
+
+ device = 'GPU' if known_args.device.upper() == 'GPU' else 'CPU'
+
+ # Fixed batch size (no right-fitting)
+ batch_size = int(known_args.inference_batch_size)
+
+ model_handler = PytorchModelHandlerTensor(
+ model_class=lambda: create_torchvision_detection_model(
+ known_args.pretrained_model_name),
+ model_params={},
+ state_dict_path=known_args.model_state_dict_path,
+ device=device,
+ inference_batch_size=batch_size,
+ inference_fn=_torchvision_detection_inference_fn,
+ )
+
+ pipeline = test_pipeline or beam.Pipeline(options=pipeline_options)
+
+ if known_args.mode == 'batch':
+ pcoll = (
+ pipeline
+ | 'ReadURIsBatch' >> beam.io.ReadFromText(known_args.input)
+ | 'FilterEmptyBatch' >> beam.Filter(lambda s: s.strip()))
+ else:
+ pcoll = (
+ pipeline
+ | 'ReadFromPubSub' >>
+ beam.io.ReadFromPubSub(subscription=known_args.pubsub_subscription)
+ | 'DecodeUTF8' >> beam.Map(lambda x: x.decode('utf-8'))
+ | 'Window' >> beam.WindowInto(
+ window.FixedWindows(known_args.window_sec),
+ trigger=beam.trigger.AfterProcessingTime(
+ known_args.trigger_proc_time_sec),
+ accumulation_mode=beam.trigger.AccumulationMode.DISCARDING,
+ allowed_lateness=0))
+
+ keyed = (pcoll | 'MakeKey' >> beam.ParDo(MakeKeyDoFn()))
+
+ preprocessed = (
+ keyed
+ | 'DecodePreprocess' >> beam.ParDo(
+ DecodePreprocessDoFn(image_size=known_args.image_size)))
+
+ to_infer = (
+ preprocessed
+ | 'ToKeyedTensor' >> beam.Map(lambda kv: (kv[0], kv[1]["tensor"])))
+
+ predictions = (
+ to_infer
+ | 'RunInference' >> RunInference(KeyedModelHandler(model_handler)))
+
+ results = (
+ predictions
+ | 'PostProcess' >> beam.ParDo(
+ PostProcessDoFn(
+ model_name=known_args.pretrained_model_name,
+ score_threshold=known_args.score_threshold,
+ max_detections=known_args.max_detections)))
+
+ method = (
+ beam.io.WriteToBigQuery.Method.FILE_LOADS if known_args.mode == 'batch'
+ else beam.io.WriteToBigQuery.Method.STREAMING_INSERTS)
+
+ if known_args.publish_to_big_query == 'true':
+ _ = (
+ results
+ | 'WriteToBigQuery' >> beam.io.WriteToBigQuery(
+ known_args.output_table,
+ schema=(
+ 'image_id:STRING, model_name:STRING, '
+ 'detections:STRING, num_detections:INT64, infer_ms:INT64'),
+ write_disposition=beam.io.BigQueryDisposition.WRITE_APPEND,
+ create_disposition=beam.io.BigQueryDisposition.CREATE_IF_NEEDED,
+ method=method))
+
+ result = pipeline.run()
+ try:
+ result.wait_until_finish(duration=1800000) # 30 min
+ finally:
+ try:
+ result.cancel()
+ except Exception:
+ logging.debug("Failed to cancel pipeline result.", exc_info=True)
+
+ if known_args.mode == 'streaming':
+ cleanup_pubsub_resources(
+ project=known_args.project,
+ topic_path=known_args.pubsub_topic,
+ subscription_path=known_args.pubsub_subscription)
+
+ return result
+
+
+if __name__ == '__main__':
+ logging.getLogger().setLevel(logging.INFO)
+ run()
diff --git
a/sdks/python/apache_beam/examples/inference/pytorch_imagenet_rightfit.py
b/sdks/python/apache_beam/examples/inference/pytorch_imagenet_rightfit.py
new file mode 100644
index 00000000000..168303cb61c
--- /dev/null
+++ b/sdks/python/apache_beam/examples/inference/pytorch_imagenet_rightfit.py
@@ -0,0 +1,537 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This pipeline performs image classification using an open-source
+PyTorch EfficientNet-B0 model optimized for T4 GPUs.
+It reads image URIs from Pub/Sub, decodes and preprocesses them in parallel,
+and runs inference with adaptive batch sizing for optimal GPU utilization.
+The pipeline targets stable and reproducible performance measurements under
+continuous load.
+Resources like Pub/Sub topic/subscription cleanup is handled programmatically.
+"""
+
+import argparse
+import io
+import json
+import logging
+import threading
+import time
+from typing import Optional
+from typing import Tuple
+
+import torch
+import torch.nn.functional as F
+
+import apache_beam as beam
+from apache_beam.io.filesystems import FileSystems
+from apache_beam.ml.inference.base import KeyedModelHandler
+from apache_beam.ml.inference.base import PredictionResult
+from apache_beam.ml.inference.base import RunInference
+from apache_beam.ml.inference.pytorch_inference import
PytorchModelHandlerTensor
+from apache_beam.options.pipeline_options import PipelineOptions
+from apache_beam.options.pipeline_options import SetupOptions
+from apache_beam.options.pipeline_options import StandardOptions
+from apache_beam.runners.runner import PipelineResult
+from apache_beam.transforms import window
+
+from google.api_core.exceptions import NotFound
+from google.cloud import pubsub_v1
+import PIL.Image as PILImage
+
+# ============ Utility & Preprocessing ============
+
+IMAGENET_MEAN = [0.485, 0.456, 0.406]
+IMAGENET_STD = [0.229, 0.224, 0.225]
+
+
+def now_millis() -> int:
+ return int(time.time() * 1000)
+
+
+def load_image_from_uri(uri: str) -> bytes:
+ with FileSystems.open(uri) as f:
+ return f.read()
+
+
+def decode_and_preprocess(image_bytes: bytes, size: int = 224) -> torch.Tensor:
+ """Decode bytes->RGB PIL->resize shorter side->center crop->normalize."""
+ with PILImage.open(io.BytesIO(image_bytes)) as img:
+ img = img.convert("RGB")
+
+ resize_size = 256
+ w, h = img.size
+ if w < h:
+ new_w = resize_size
+ new_h = int(h * resize_size / w)
+ else:
+ new_h = resize_size
+ new_w = int(w * resize_size / h)
+
+ img = img.resize((new_w, new_h))
+
+ w, h = img.size
+ left = (w - size) // 2
+ top = (h - size) // 2
+ img = img.crop((left, top, left + size, top + size))
+
+ import numpy as np
+ mean = np.array(IMAGENET_MEAN, dtype=np.float32)
+ std = np.array(IMAGENET_STD, dtype=np.float32)
+
+ arr = np.asarray(img).astype("float32") / 255.0
+ arr = (arr - mean) / std
+ arr = np.transpose(arr, (2, 0, 1)).astype("float32")
+ return torch.from_numpy(arr).float()
+
+
+class MakeKeyDoFn(beam.DoFn):
+ """Produce (image_id, payload) stable for dedup & BQ insertId."""
+ def __init__(self, input_mode: str):
+ self.input_mode = input_mode
+
+ def process(self, element: str | bytes):
+ # Input can be raw bytes from Pub/Sub or a GCS URI string, depends on mode
+ if self.input_mode == "bytes":
+ # element is bytes message, assume it includes
+ # {"image_id": "...", "bytes": base64?} or just raw bytes.
+ import hashlib
+ b = element if isinstance(element,
+ (bytes,
+ bytearray)) else element.encode('utf-8')
+ image_id = hashlib.sha1(b).hexdigest()
+ yield image_id, b
+ else:
+ # gcs_uris: element is uri string; image_id = sha1(uri)
+ import hashlib
+ uri = element.decode("utf-8") if isinstance(
+ element, (bytes, bytearray)) else str(element)
+ image_id = hashlib.sha1(uri.encode("utf-8")).hexdigest()
+ yield image_id, uri
+
+
+class DecodePreprocessDoFn(beam.DoFn):
+ """Turn (image_id, bytes|uri) -> (image_id, torch.Tensor)"""
+ def __init__(self, input_mode: str, image_size: int = 224):
+ self.input_mode = input_mode
+ self.image_size = image_size
+
+ def process(self, kv: Tuple[str, object]):
+ image_id, payload = kv
+ start = now_millis()
+
+ try:
+ if self.input_mode == "bytes":
+ b = payload if isinstance(payload,
+ (bytes, bytearray)) else bytes(payload)
+ else:
+ uri = payload if isinstance(payload, str) else payload.decode("utf-8")
+ b = load_image_from_uri(uri)
+
+ tensor = decode_and_preprocess(b, self.image_size)
+ preprocess_ms = now_millis() - start
+ yield image_id, {"tensor": tensor, "preprocess_ms": preprocess_ms}
+ except Exception as e:
+ logging.warning("Decode failed for %s: %s", image_id, e)
+ return
+
+
+class PostProcessDoFn(beam.DoFn):
+ """PredictionResult -> dict row for BQ."""
+ def __init__(self, top_k: int, model_name: str):
+ self.top_k = top_k
+ self.model_name = model_name
+
+ def process(self, kv: Tuple[str, PredictionResult]):
+ image_id, pred = kv
+
+ # pred can be PredictionResult OR raw inference object.
+ inference_obj = pred.inference if hasattr(pred, "inference") else pred
+
+ # inference_obj can be dict {'logits': tensor} OR tensor directly.
+ if isinstance(inference_obj, dict):
+ logits = inference_obj.get("logits", None)
+ if logits is None:
+ raise ValueError(
+ f"Unable to find 'logits' in model output. "
+ f"Available keys: {list(inference_obj.keys())}")
+ else:
+ logits = inference_obj
+
+ if not isinstance(logits, torch.Tensor):
+ logging.warning(
+ "Unexpected logits type for %s: %s", image_id, type(logits))
+ return
+
+ # Ensure shape [1, C]
+ if logits.ndim == 1:
+ logits = logits.unsqueeze(0)
+
+ probs = F.softmax(logits, dim=-1) # [B, C]
+ values, indices = torch.topk(
+ probs, k=min(self.top_k, probs.shape[-1]), dim=-1
+ )
+
+ topk = [{
+ "class_id": int(idx.item()), "score": float(val.item())
+ } for idx, val in zip(indices[0], values[0])]
+
+ yield {
+ "image_id": image_id,
+ "model_name": self.model_name,
+ "topk": json.dumps(topk),
+ "infer_ms": now_millis(),
+ }
+
+
+# ============ Args & Helpers ============
+
+
+def parse_known_args(argv):
+ parser = argparse.ArgumentParser()
+ # I/O & runtime
+ parser.add_argument(
+ '--project', default='apache-beam-testing', help='GCP project ID')
+ parser.add_argument(
+ '--mode', default='streaming', choices=['streaming', 'batch'])
+ parser.add_argument(
+ '--output_table',
+ required=True,
+ help='BigQuery output table: dataset.table')
+ parser.add_argument(
+ '--publish_to_big_query', default='true', choices=['true', 'false'])
+ parser.add_argument(
+ '--input_mode', default='gcs_uris', choices=['gcs_uris', 'bytes'])
+ parser.add_argument(
+ '--input',
+ required=True,
+ help='GCS path to file with URIs (for load) OR unused for bytes')
+ parser.add_argument(
+ '--pubsub_topic',
+ default='projects/apache-beam-testing/topics/images_topic')
+ parser.add_argument(
+ '--pubsub_subscription',
+ default='projects/apache-beam-testing/subscriptions/images_subscription')
+ parser.add_argument(
+ '--feeder_start_delay_sec',
+ type=int,
+ default=900,
+ help=(
+ 'Delay before starting the feeder pipeline that reads URIs from GCS '
+ 'and publishes them to Pub/Sub. This delay allows the main streaming
'
+ 'pipeline workers to start and scale before data ingestion begins.'),
+ )
+
+ # Model & inference
+ parser.add_argument(
+ '--pretrained_model_name',
+ default='efficientnet_b0',
+ help='OSS model name (e.g., efficientnet_b0|mobilenetv3_large_100)')
+ parser.add_argument(
+ '--model_state_dict_path',
+ default=None,
+ help='Optional state_dict to load')
+ parser.add_argument('--device', default='GPU', choices=['CPU', 'GPU'])
+ parser.add_argument('--image_size', type=int, default=224)
+ parser.add_argument('--top_k', type=int, default=5)
+ parser.add_argument(
+ '--inference_batch_size',
+ default='16',
+ help='int or "auto"; auto tries 64→32→16')
+
+ # Windows
+ parser.add_argument('--window_sec', type=int, default=60)
+ parser.add_argument('--trigger_proc_time_sec', type=int, default=30)
+
+ known_args, pipeline_args = parser.parse_known_args(argv)
+ return known_args, pipeline_args
+
+
+def ensure_pubsub_resources(
+ project: str, topic_path: str, subscription_path: str):
+ publisher = pubsub_v1.PublisherClient()
+ subscriber = pubsub_v1.SubscriberClient()
+
+ try:
+ publisher.get_topic(request={"topic": topic_path})
+ except NotFound:
+ publisher.create_topic(name=topic_path)
+
+ try:
+ subscriber.get_subscription(request={"subscription": subscription_path})
+ except NotFound:
+ subscriber.create_subscription(name=subscription_path, topic=topic_path)
+
+
+def cleanup_pubsub_resources(
+ project: str, topic_path: str, subscription_path: str):
+ publisher = pubsub_v1.PublisherClient()
+ subscriber = pubsub_v1.SubscriberClient()
+
+ try:
+ subscriber.delete_subscription(request={"subscription": subscription_path})
+ logging.info(f"Deleted subscription: {subscription_path}")
+ except NotFound:
+ logging.info(f"Subscription already deleted: {subscription_path}")
+
+ try:
+ publisher.delete_topic(request={"topic": topic_path})
+ logging.info(f"Deleted topic: {topic_path}")
+ except NotFound:
+ logging.info(f"Topic already deleted: {topic_path}")
+
+
+def override_or_add(args, flag, value):
+ if flag in args:
+ idx = args.index(flag)
+ args[idx + 1] = str(value)
+ else:
+ args.extend([flag, str(value)])
+
+
+# ============ Model factory (timm) ============
+
+
+def create_timm_m(model_name: str, num_classes: int = 1000):
+ import timm
+ model = timm.create_model(
+ model_name, pretrained=True, num_classes=num_classes)
+ model.eval()
+ return model
+
+
+def pick_batch_size(arg: str) -> Optional[int]:
+ if isinstance(arg, str) and arg.lower() == 'auto':
+ return None
+ try:
+ return int(arg)
+ except Exception:
+ return None
+
+
+class RightFittingPytorchModelHandlerTensor(PytorchModelHandlerTensor):
+ def __init__(self, batch_sizes_to_try, image_size, *args, **kwargs):
+ self._batch_sizes_to_try = batch_sizes_to_try
+ self._rightfit_image_size = image_size
+ self._selected_batch_size = batch_sizes_to_try[0]
+ super().__init__(*args, **kwargs)
+
+ def load_model(self):
+ model = super().load_model()
+ last_err = None
+
+ for bs in self._batch_sizes_to_try:
+ try:
+ model_device = next(model.parameters()).device
+ dummy = torch.zeros(
+ (bs, 3, self._rightfit_image_size, self._rightfit_image_size),
+ dtype=torch.float32,
+ device=model_device)
+
+ with torch.no_grad():
+ model(dummy)
+
+ self._selected_batch_size = bs
+ logging.info("Selected inference batch size: %s", bs)
+ return model
+ except RuntimeError as e:
+ last_err = e
+ logging.warning("Batch size %s failed during worker warmup: %s", bs, e)
+
+ del dummy
+ if torch.cuda.is_available():
+ torch.cuda.empty_cache()
+
+ raise RuntimeError(
+ f"No valid inference batch size found from {self._batch_sizes_to_try}"
+ ) from last_err
+
+ def run_inference(self, batch, model, inference_args=None):
+ results = []
+
+ for i in range(0, len(batch), self._selected_batch_size):
+ sub_batch = batch[i:i + self._selected_batch_size]
+ results.extend(super().run_inference(
+ sub_batch,
+ model,
+ inference_args,
+ ))
+
+ return results
+
+
+# ============ Load pipeline ============
+
+
+def run_load_pipeline(known_args, pipeline_args):
+ """Reads GCS file with URIs and publishes them to Pub/Sub (for streaming)."""
+
+ pipeline_args = list(pipeline_args)
+ # enforce smaller/CPU-only defaults for feeder
+ override_or_add(pipeline_args, '--device', 'CPU')
+ override_or_add(pipeline_args, '--num_workers', '5')
+ override_or_add(pipeline_args, '--max_num_workers', '10')
+ override_or_add(
+ pipeline_args, '--job_name', f"images-load-pubsub-{int(time.time())}")
+ override_or_add(pipeline_args, '--project', known_args.project)
+ pipeline_args = [
+ arg for arg in pipeline_args if not arg.startswith("--experiments")
+ ]
+
+ pipeline_options = PipelineOptions(pipeline_args)
+ pipeline = beam.Pipeline(options=pipeline_options)
+
+ _ = (
+ pipeline
+ | 'ReadGCSFile' >> beam.io.ReadFromText(known_args.input)
+ | 'FilterEmpty' >> beam.Filter(lambda line: line.strip())
+ | 'ToBytes' >> beam.Map(lambda line: line.encode('utf-8'))
+ | 'ToPubSub' >> beam.io.WriteToPubSub(topic=known_args.pubsub_topic))
+ return pipeline.run()
+
+
+# ============ Main pipeline ============
+
+
+def run(
+ argv=None, save_main_session=True, test_pipeline=None) -> PipelineResult:
+ known_args, pipeline_args = parse_known_args(argv)
+
+ if known_args.mode == 'streaming':
+ ensure_pubsub_resources(
+ project=known_args.project,
+ topic_path=known_args.pubsub_topic,
+ subscription_path=known_args.pubsub_subscription)
+
+ # Start feeder thread that reads URIs from GCS and fills Pub/Sub.
+ # Delay is used to allow the main streaming pipeline workers to start
+ # and autoscale before the feeder pipeline begins publishing messages.
+ threading.Thread(
+ target=lambda: (
+ time.sleep(known_args.feeder_start_delay_sec), run_load_pipeline(
+ known_args, pipeline_args)),
+ daemon=True).start()
+
+ # StandardOptions
+ pipeline_options = PipelineOptions(pipeline_args)
+ pipeline_options.view_as(SetupOptions).save_main_session = save_main_session
+ pipeline_options.view_as(StandardOptions).streaming = (
+ known_args.mode == 'streaming')
+
+ # Build model handler with right-fitting batch size
+ desired_batch = pick_batch_size(known_args.inference_batch_size)
+
+ # Device
+ device = 'GPU' if known_args.device.upper() == 'GPU' else 'CPU'
+
+ tried = [64, 32, 16, 8] if desired_batch is None else [desired_batch]
+
+ model_handler = RightFittingPytorchModelHandlerTensor(
+ batch_sizes_to_try=tried,
+ image_size=known_args.image_size,
+ device=device,
+ model_class=lambda: create_timm_m(known_args.pretrained_model_name),
+ model_params={},
+ state_dict_path=known_args.model_state_dict_path,
+ inference_batch_size=tried[0],
+ )
+
+ pipeline = test_pipeline or beam.Pipeline(options=pipeline_options)
+
+ if known_args.mode == 'batch':
+ pcoll = (
+ pipeline
+ | 'ReadURIsBatch' >> beam.io.ReadFromText(known_args.input)
+ | 'FilterEmptyBatch' >> beam.Filter(lambda s: s.strip()))
+ else:
+ pcoll = (
+ pipeline
+ | 'ReadFromPubSub' >>
+ beam.io.ReadFromPubSub(subscription=known_args.pubsub_subscription)
+ | 'DecodeUTF8' >> beam.Map(lambda x: x.decode('utf-8'))
+ | 'Window' >> beam.WindowInto(
+ window.FixedWindows(known_args.window_sec),
+ trigger=beam.trigger.AfterProcessingTime(
+ known_args.trigger_proc_time_sec),
+ accumulation_mode=beam.trigger.AccumulationMode.DISCARDING,
+ allowed_lateness=0))
+
+ keyed = (
+ pcoll
+ | 'MakeKey' >> beam.ParDo(MakeKeyDoFn(input_mode=known_args.input_mode)))
+
+ preprocessed = (
+ keyed
+ | 'DecodePreprocess' >> beam.ParDo(
+ DecodePreprocessDoFn(
+ input_mode=known_args.input_mode,
+ image_size=known_args.image_size)))
+
+ to_infer = (
+ preprocessed
+ |
+ 'ToKeyedTensor' >> beam.Map(lambda kv: (kv[0], kv[1]["tensor"].float())))
+
+ run_inference = RunInference(KeyedModelHandler(model_handler))
+ if known_args.device.upper() == 'GPU':
+ run_inference = run_inference.with_resource_hints(
+ accelerator="type:nvidia-tesla-t4;count:1;install-nvidia-driver")
+ predictions = (
+ to_infer
+ | 'Reshuffle' >> beam.Reshuffle()
+ | 'RunInference' >> run_inference)
+
+ results = (
+ predictions
+ | 'PostProcess' >> beam.ParDo(
+ PostProcessDoFn(
+ top_k=known_args.top_k,
+ model_name=known_args.pretrained_model_name)))
+
+ method = (
+ beam.io.WriteToBigQuery.Method.FILE_LOADS if known_args.mode == 'batch'
+ else beam.io.WriteToBigQuery.Method.STREAMING_INSERTS)
+
+ if known_args.publish_to_big_query == 'true':
+ _ = (
+ results
+ | 'WriteToBigQuery' >> beam.io.WriteToBigQuery(
+ known_args.output_table,
+ schema=
+ 'image_id:STRING, model_name:STRING, topk:STRING, infer_ms:INT64',
+ write_disposition=beam.io.BigQueryDisposition.WRITE_APPEND,
+ create_disposition=beam.io.BigQueryDisposition.CREATE_IF_NEEDED,
+ method=method))
+
+ result = pipeline.run()
+ try:
+ result.wait_until_finish(duration=1800000) # 30 min
+ finally:
+ try:
+ result.cancel()
+ except Exception:
+ logging.debug("Failed to cancel pipeline result.", exc_info=True)
+
+ if known_args.mode == 'streaming':
+ cleanup_pubsub_resources(
+ project=known_args.project,
+ topic_path=known_args.pubsub_topic,
+ subscription_path=known_args.pubsub_subscription)
+
+ return result
+
+
+if __name__ == '__main__':
+ logging.getLogger().setLevel(logging.INFO)
+ run()
diff --git
a/sdks/python/apache_beam/ml/inference/pytorch_image_captioning_requirements.txt
b/sdks/python/apache_beam/ml/inference/pytorch_image_captioning_requirements.txt
new file mode 100644
index 00000000000..726cc147a0a
--- /dev/null
+++
b/sdks/python/apache_beam/ml/inference/pytorch_image_captioning_requirements.txt
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+torch>=2.6.0,<2.8.0
+Pillow>=10.0.0
+numpy>=1.25.0
+transformers>=4.41.0,<5.0.0
+accelerate>=0.30.0
+tokenizers>=0.19.0
+safetensors>=0.4.3
+protobuf>=4.25.1
+requests>=2.31.0
+google-cloud-monitoring>=2.27.0
diff --git
a/sdks/python/apache_beam/ml/inference/pytorch_image_object_detection_requirements.txt
b/sdks/python/apache_beam/ml/inference/pytorch_image_object_detection_requirements.txt
new file mode 100644
index 00000000000..c3ce392d1a4
--- /dev/null
+++
b/sdks/python/apache_beam/ml/inference/pytorch_image_object_detection_requirements.txt
@@ -0,0 +1,24 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+torch>=2.2.0,<2.8.0
+torchvision>=0.17.0,<0.21.0
+Pillow>=10.0.0
+numpy>=1.25.0
+google-cloud-monitoring>=2.27.0
+protobuf>=4.25.1
+requests>=2.31.0
diff --git
a/sdks/python/apache_beam/ml/inference/pytorch_rightfit_requirements.txt
b/sdks/python/apache_beam/ml/inference/pytorch_rightfit_requirements.txt
new file mode 100644
index 00000000000..2b2916c577e
--- /dev/null
+++ b/sdks/python/apache_beam/ml/inference/pytorch_rightfit_requirements.txt
@@ -0,0 +1,26 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+torch>=2.2.0,<2.8.0
+torchvision>=0.17.0,<0.21.0
+timm>=1.0.7
+Pillow>=10.0.0
+numpy>=1.25.0
+google-cloud-pubsub>=2.15.0
+google-cloud-monitoring>=2.27.0
+protobuf>=4.25.1
+requests>=2.31.0
diff --git
a/sdks/python/apache_beam/testing/benchmarks/inference/pytorch_image_captioning_benchmarks.py
b/sdks/python/apache_beam/testing/benchmarks/inference/pytorch_image_captioning_benchmarks.py
new file mode 100644
index 00000000000..2960bfb7468
--- /dev/null
+++
b/sdks/python/apache_beam/testing/benchmarks/inference/pytorch_image_captioning_benchmarks.py
@@ -0,0 +1,42 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# pytype: skip-file
+
+import logging
+
+from apache_beam.examples.inference import pytorch_image_captioning
+from apache_beam.testing.load_tests.dataflow_cost_benchmark import
DataflowCostBenchmark
+
+
+class PytorchImageCaptioningBenchmarkTest(DataflowCostBenchmark):
+ def __init__(self):
+ self.metrics_namespace = 'BeamML_PyTorch'
+ super().__init__(
+ metrics_namespace=self.metrics_namespace,
+ pcollection='PostProcess.out0')
+
+ def test(self):
+ extra_opts = {}
+ extra_opts['input'] = self.pipeline.get_option('input_file')
+ self.result = pytorch_image_captioning.run(
+ self.pipeline.get_full_options_as_args(**extra_opts),
+ test_pipeline=self.pipeline)
+
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.INFO)
+ PytorchImageCaptioningBenchmarkTest().run()
diff --git
a/sdks/python/apache_beam/testing/benchmarks/inference/pytorch_image_object_detection_benchmarks.py
b/sdks/python/apache_beam/testing/benchmarks/inference/pytorch_image_object_detection_benchmarks.py
new file mode 100644
index 00000000000..3bfd5daec18
--- /dev/null
+++
b/sdks/python/apache_beam/testing/benchmarks/inference/pytorch_image_object_detection_benchmarks.py
@@ -0,0 +1,42 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# pytype: skip-file
+
+import logging
+
+from apache_beam.examples.inference import pytorch_image_object_detection
+from apache_beam.testing.load_tests.dataflow_cost_benchmark import
DataflowCostBenchmark
+
+
+class PytorchImageObjectDetectionBenchmarkTest(DataflowCostBenchmark):
+ def __init__(self):
+ self.metrics_namespace = 'BeamML_PyTorch'
+ super().__init__(
+ metrics_namespace=self.metrics_namespace,
+ pcollection='PostProcess.out0')
+
+ def test(self):
+ extra_opts = {}
+ extra_opts['input'] = self.pipeline.get_option('input_file')
+ self.result = pytorch_image_object_detection.run(
+ self.pipeline.get_full_options_as_args(**extra_opts),
+ test_pipeline=self.pipeline)
+
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.INFO)
+ PytorchImageObjectDetectionBenchmarkTest().run()
diff --git
a/sdks/python/apache_beam/testing/benchmarks/inference/pytorch_imagenet_rightfit_benchmarks.py
b/sdks/python/apache_beam/testing/benchmarks/inference/pytorch_imagenet_rightfit_benchmarks.py
new file mode 100644
index 00000000000..1528711a801
--- /dev/null
+++
b/sdks/python/apache_beam/testing/benchmarks/inference/pytorch_imagenet_rightfit_benchmarks.py
@@ -0,0 +1,42 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# pytype: skip-file
+
+import logging
+
+from apache_beam.examples.inference import pytorch_imagenet_rightfit
+from apache_beam.testing.load_tests.dataflow_cost_benchmark import
DataflowCostBenchmark
+
+
+class PytorchImagenetRightfitBenchmarkTest(DataflowCostBenchmark):
+ def __init__(self):
+ self.metrics_namespace = 'BeamML_PyTorch'
+ super().__init__(
+ metrics_namespace=self.metrics_namespace,
+ pcollection='PostProcess.out0')
+
+ def test(self):
+ extra_opts = {}
+ extra_opts['input'] = self.pipeline.get_option('input_file')
+ self.result = pytorch_imagenet_rightfit.run(
+ self.pipeline.get_full_options_as_args(**extra_opts),
+ test_pipeline=self.pipeline)
+
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.INFO)
+ PytorchImagenetRightfitBenchmarkTest().run()
diff --git a/website/www/site/content/en/performance/_index.md
b/website/www/site/content/en/performance/_index.md
index 347b7c1110c..db05afd4cbb 100644
--- a/website/www/site/content/en/performance/_index.md
+++ b/website/www/site/content/en/performance/_index.md
@@ -46,6 +46,14 @@ See the following pages for performance measures recorded
when running various B
## Streaming
- [PyTorch Sentiment Analysis Streaming DistilBERT
base](/performance/pytorchbertsentimentstreaming)
+- [PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting)
CPU](/performance/pytorchimagenetrightfitcpu)
+- [PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting
Exactly-once) CPU](/performance/pytorchimagenetrightfitoncecpu)
+- [PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting)
GPU](/performance/pytorchimagenetrightfitgpu)
+- [PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting
Exactly-once)](/performance/pytorchimagenetrightfitoncegpu)
+- [PyTorch Image Captioning BLIP + CLIP Streaming
CPU](/performance/pytorchimagecaptioningstreamingcpu)
+- [PyTorch Image Captioning BLIP + CLIP Streaming
GPU](/performance/pytorchimagecaptioningstreaminggpu)
+- [PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming
CPU](/performance/pytorchimageobjectdetectionstreamingcpu)
+- [PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming
GPU](/performance/pytorchimageobjectdetectionstreaminggpu)
- [Table Row Inference Sklearn
Streaming](/performance/tablerowinferencestreaming)
## Batch
@@ -59,6 +67,10 @@ See the following pages for performance measures recorded
when running various B
- [TensorFlow MNIST Image Classification](/performance/tensorflowmnist)
- [VLLM Gemma Batch Completion Tesla T4 GPU](/performance/vllmgemmabatchtesla)
- [Table Row Inference Sklearn Batch](/performance/tablerowinference)
+- [PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch
CPU](/performance/pytorchimageobjectdetectionbatchcpu)
+- [PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch
GPU](/performance/pytorchimageobjectdetectionbatchgpu)
+- [PyTorch Image Captioning BLIP + CLIP Batch
CPU](/performance/pytorchimagecaptioningbatchcpu)
+- [PyTorch Image Captioning BLIP + CLIP Batch
GPU](/performance/pytorchimagecaptioningbatchgpu)
- [MLTransform Generate Vocab (batch)](/performance/mltransformvocab)
- [MLTransform One-Hot Encoding](/performance/mltransformonehot)
- [MLTransform Text Embedding](/performance/mltransform-text-embedding)
diff --git
a/website/www/site/content/en/performance/pytorchimagecaptioningbatchcpu/_index.md
b/website/www/site/content/en/performance/pytorchimagecaptioningbatchcpu/_index.md
new file mode 100644
index 00000000000..d5d5f094e55
--- /dev/null
+++
b/website/www/site/content/en/performance/pytorchimagecaptioningbatchcpu/_index.md
@@ -0,0 +1,41 @@
+---
+title: "PyTorch Image Captioning BLIP + CLIP Batch CPU Performance"
+---
+
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# PyTorch Image Captioning BLIP + CLIP Batch CPU
+
+**Model**: PyTorch Image Captioning — BLIP (candidate generation) + CLIP
(ranking)
+**Accelerator**: CPU only
+**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM)
+
+This batch pipeline performs image captioning using a multi-model open-source
PyTorch approach.
+It first generates multiple candidate captions per image using a BLIP model,
then ranks these candidates with a CLIP model based on image-text similarity.
+
+The following graphs show various metrics when running PyTorch Image
Captioning BLIP + CLIP Batch CPU pipeline.
+See the [glossary](/performance/glossary) for definitions.
+
+Full pipeline implementation is available
[here](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/inference/pytorch_image_captioning.py).
+
+## What is the estimated cost to run the pipeline?
+
+{{< performance_looks io="pytorchimagecaptioningbatchcpu"
read_or_write="write" section="cost" >}}
+
+## How has various metrics changed when running the pipeline for different
Beam SDK versions?
+
+{{< performance_looks io="pytorchimagecaptioningbatchcpu"
read_or_write="write" section="version" >}}
+
+## How has various metrics changed over time when running the pipeline?
+
+{{< performance_looks io="pytorchimagecaptioningbatchcpu"
read_or_write="write" section="date" >}}
diff --git
a/website/www/site/content/en/performance/pytorchimagecaptioningbatchgpu/_index.md
b/website/www/site/content/en/performance/pytorchimagecaptioningbatchgpu/_index.md
new file mode 100644
index 00000000000..d0d12bf3f54
--- /dev/null
+++
b/website/www/site/content/en/performance/pytorchimagecaptioningbatchgpu/_index.md
@@ -0,0 +1,41 @@
+---
+title: "PyTorch Image Captioning BLIP + CLIP Batch GPU Performance"
+---
+
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# PyTorch Image Captioning BLIP + CLIP Batch GPU
+
+**Model**: PyTorch Image Captioning — BLIP (candidate generation) + CLIP
(ranking)
+**Accelerator**: Tesla T4 GPU
+**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM)
+
+This batch pipeline performs image captioning using a multi-model open-source
PyTorch approach.
+It first generates multiple candidate captions per image using a BLIP model,
then ranks these candidates with a CLIP model based on image-text similarity.
+
+The following graphs show various metrics when running PyTorch Image
Captioning BLIP + CLIP Batch GPU pipeline.
+See the [glossary](/performance/glossary) for definitions.
+
+Full pipeline implementation is available
[here](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/inference/pytorch_image_captioning.py).
+
+## What is the estimated cost to run the pipeline?
+
+{{< performance_looks io="pytorchimagecaptioningbatchgpu"
read_or_write="write" section="cost" >}}
+
+## How has various metrics changed when running the pipeline for different
Beam SDK versions?
+
+{{< performance_looks io="pytorchimagecaptioningbatchgpu"
read_or_write="write" section="version" >}}
+
+## How has various metrics changed over time when running the pipeline?
+
+{{< performance_looks io="pytorchimagecaptioningbatchgpu"
read_or_write="write" section="date" >}}
diff --git
a/website/www/site/content/en/performance/pytorchimagecaptioningstreamingcpu/_index.md
b/website/www/site/content/en/performance/pytorchimagecaptioningstreamingcpu/_index.md
new file mode 100644
index 00000000000..41223f1bf0b
--- /dev/null
+++
b/website/www/site/content/en/performance/pytorchimagecaptioningstreamingcpu/_index.md
@@ -0,0 +1,41 @@
+---
+title: "PyTorch Image Captioning BLIP + CLIP Streaming CPU Performance"
+---
+
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# PyTorch Image Captioning BLIP + CLIP Streaming CPU
+
+**Model**: PyTorch Image Captioning — BLIP (candidate generation) + CLIP
(ranking)
+**Accelerator**: CPU only
+**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM)
+
+This streaming pipeline performs image captioning using a multi-model
open-source PyTorch approach.
+It first generates multiple candidate captions per image using a BLIP model,
then ranks these candidates with a CLIP model based on image-text similarity.
+
+The following graphs show various metrics when running PyTorch Image
Captioning BLIP + CLIP Streaming CPU pipeline.
+See the [glossary](/performance/glossary) for definitions.
+
+Full pipeline implementation is available
[here](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/inference/pytorch_image_captioning.py).
+
+## What is the estimated cost to run the pipeline?
+
+{{< performance_looks io="pytorchimagecaptioningstreamingcpu"
read_or_write="write" section="cost" >}}
+
+## How has various metrics changed when running the pipeline for different
Beam SDK versions?
+
+{{< performance_looks io="pytorchimagecaptioningstreamingcpu"
read_or_write="write" section="version" >}}
+
+## How has various metrics changed over time when running the pipeline?
+
+{{< performance_looks io="pytorchimagecaptioningstreamingcpu"
read_or_write="write" section="date" >}}
diff --git
a/website/www/site/content/en/performance/pytorchimagecaptioningstreaminggpu/_index.md
b/website/www/site/content/en/performance/pytorchimagecaptioningstreaminggpu/_index.md
new file mode 100644
index 00000000000..b7694baa82e
--- /dev/null
+++
b/website/www/site/content/en/performance/pytorchimagecaptioningstreaminggpu/_index.md
@@ -0,0 +1,41 @@
+---
+title: "PyTorch Image Captioning BLIP + CLIP Streaming GPU Performance"
+---
+
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# PyTorch Image Captioning BLIP + CLIP Streaming GPU
+
+**Model**: PyTorch Image Captioning — BLIP (candidate generation) + CLIP
(ranking)
+**Accelerator**: Tesla T4 GPU
+**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM)
+
+This streaming pipeline performs image captioning using a multi-model
open-source PyTorch approach.
+It first generates multiple candidate captions per image using a BLIP model,
then ranks these candidates with a CLIP model based on image-text similarity.
+
+The following graphs show various metrics when running PyTorch Image
Captioning BLIP + CLIP Streaming GPU pipeline.
+See the [glossary](/performance/glossary) for definitions.
+
+Full pipeline implementation is available
[here](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/inference/pytorch_image_captioning.py).
+
+## What is the estimated cost to run the pipeline?
+
+{{< performance_looks io="pytorchimagecaptioningstreaminggpu"
read_or_write="write" section="cost" >}}
+
+## How has various metrics changed when running the pipeline for different
Beam SDK versions?
+
+{{< performance_looks io="pytorchimagecaptioningstreaminggpu"
read_or_write="write" section="version" >}}
+
+## How has various metrics changed over time when running the pipeline?
+
+{{< performance_looks io="pytorchimagecaptioningstreaminggpu"
read_or_write="write" section="date" >}}
diff --git
a/website/www/site/content/en/performance/pytorchimagenetrightfitcpu/_index.md
b/website/www/site/content/en/performance/pytorchimagenetrightfitcpu/_index.md
new file mode 100644
index 00000000000..9b7b26d9b3e
--- /dev/null
+++
b/website/www/site/content/en/performance/pytorchimagenetrightfitcpu/_index.md
@@ -0,0 +1,43 @@
+---
+title: "PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting)
CPU Performance"
+---
+
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting)
+
+**Model**: PyTorch Image Classification — EfficientNet-B0 (pretrained on
ImageNet)
+**Accelerator**: CPU only
+**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM)
+
+This streaming pipeline performs image classification using an open-source
PyTorch EfficientNet-B0 model.
+It reads image URIs from Pub/Sub, decodes and preprocesses them in parallel,
and runs inference with adaptive batch sizing.
+
+The following graphs show various metrics when running the PyTorch Image
Classification EfficientNet-B0 Streaming (Right-fitting) CPU pipeline.
+See the [glossary](/performance/glossary) for definitions.
+
+Full pipeline implementation is available
[here](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/inference/pytorch_imagenet_rightfit.py).
+
+## What is the estimated cost to run the pipeline?
+
+{{< performance_looks io="pytorchimagenetrightfitcpu" read_or_write="write"
section="cost" >}}
+
+## How has various metrics changed when running the pipeline for different
Beam SDK versions?
+
+{{< performance_looks io="pytorchimagenetrightfitcpu" read_or_write="write"
section="version" >}}
+
+## How has various metrics changed over time when running the pipeline?
+
+{{< performance_looks io="pytorchimagenetrightfitcpu" read_or_write="write"
section="date" >}}
diff --git
a/website/www/site/content/en/performance/pytorchimagenetrightfitgpu/_index.md
b/website/www/site/content/en/performance/pytorchimagenetrightfitgpu/_index.md
new file mode 100644
index 00000000000..fefd149b841
--- /dev/null
+++
b/website/www/site/content/en/performance/pytorchimagenetrightfitgpu/_index.md
@@ -0,0 +1,43 @@
+---
+title: "PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting)
GPU Performance"
+---
+
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting)
+
+**Model**: PyTorch Image Classification — EfficientNet-B0 (pretrained on
ImageNet)
+**Accelerator**: Tesla T4 GPU (right-fitted batch size 64 → 32 → 16 → 8)
+**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM)
+
+This streaming pipeline performs image classification using an open-source
PyTorch EfficientNet-B0 model optimized for T4 GPUs.
+It reads image URIs from Pub/Sub, decodes and preprocesses them in parallel,
and runs inference with adaptive batch sizing for optimal GPU utilization.
+
+The following graphs show various metrics when running the PyTorch Image
Classification EfficientNet-B0 Streaming (Right-fitting) GPU pipeline.
+See the [glossary](/performance/glossary) for definitions.
+
+Full pipeline implementation is available
[here](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/inference/pytorch_imagenet_rightfit.py).
+
+## What is the estimated cost to run the pipeline?
+
+{{< performance_looks io="pytorchimagenetrightfitgpu" read_or_write="write"
section="cost" >}}
+
+## How has various metrics changed when running the pipeline for different
Beam SDK versions?
+
+{{< performance_looks io="pytorchimagenetrightfitgpu" read_or_write="write"
section="version" >}}
+
+## How has various metrics changed over time when running the pipeline?
+
+{{< performance_looks io="pytorchimagenetrightfitgpu" read_or_write="write"
section="date" >}}
diff --git
a/website/www/site/content/en/performance/pytorchimagenetrightfitoncecpu/_index.md
b/website/www/site/content/en/performance/pytorchimagenetrightfitoncecpu/_index.md
new file mode 100644
index 00000000000..8e0cf82c349
--- /dev/null
+++
b/website/www/site/content/en/performance/pytorchimagenetrightfitoncecpu/_index.md
@@ -0,0 +1,44 @@
+---
+title: "PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting,
Exactly-once) CPU Performance"
+---
+
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting)
+
+**Model**: PyTorch Image Classification — EfficientNet-B0 (pretrained on
ImageNet)
+**Accelerator**: CPU only
+**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM)
+
+This streaming pipeline performs image classification using an open-source
PyTorch EfficientNet-B0 model.
+It reads image URIs from Pub/Sub, decodes and preprocesses them in parallel,
and runs inference with adaptive batch sizing.
+The pipeline ensures exactly-once semantics via stateful deduplication and
idempotent BigQuery writes, allowing stable and reproducible performance
measurements under continuous load.
+
+The following graphs show various metrics when running the PyTorch Image
Classification EfficientNet-B0 Streaming (Right-fitting, Exactly-once) CPU
pipeline.
+See the [glossary](/performance/glossary) for definitions.
+
+Full pipeline implementation is available
[here](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/inference/pytorch_imagenet_rightfit.py).
+
+## What is the estimated cost to run the pipeline?
+
+{{< performance_looks io="pytorchimagenetrightfitoncecpu"
read_or_write="write" section="cost" >}}
+
+## How has various metrics changed when running the pipeline for different
Beam SDK versions?
+
+{{< performance_looks io="pytorchimagenetrightfitoncecpu"
read_or_write="write" section="version" >}}
+
+## How has various metrics changed over time when running the pipeline?
+
+{{< performance_looks io="pytorchimagenetrightfitoncecpu"
read_or_write="write" section="date" >}}
diff --git
a/website/www/site/content/en/performance/pytorchimagenetrightfitoncegpu/_index.md
b/website/www/site/content/en/performance/pytorchimagenetrightfitoncegpu/_index.md
new file mode 100644
index 00000000000..85a55b27955
--- /dev/null
+++
b/website/www/site/content/en/performance/pytorchimagenetrightfitoncegpu/_index.md
@@ -0,0 +1,44 @@
+---
+title: "PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting,
Exactly-once) GPU Performance"
+---
+
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# PyTorch Image Classification EfficientNet-B0 Streaming (Right-fitting)
+
+**Model**: PyTorch Image Classification — EfficientNet-B0 (pretrained on
ImageNet)
+**Accelerator**: Tesla T4 GPU (right-fitted batch size 64 → 32 → 16 → 8)
+**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM)
+
+This streaming pipeline performs image classification using an open-source
PyTorch EfficientNet-B0 model optimized for T4 GPUs.
+It reads image URIs from Pub/Sub, decodes and preprocesses them in parallel,
and runs inference with adaptive batch sizing for optimal GPU utilization.
+The pipeline ensures exactly-once semantics via stateful deduplication and
idempotent BigQuery writes, allowing stable and reproducible performance
measurements under continuous load.
+
+The following graphs show various metrics when running the PyTorch Image
Classification EfficientNet-B0 Streaming (Right-fitting, Exactly-once) GPU
pipeline.
+See the [glossary](/performance/glossary) for definitions.
+
+Full pipeline implementation is available
[here](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/inference/pytorch_imagenet_rightfit.py).
+
+## What is the estimated cost to run the pipeline?
+
+{{< performance_looks io="pytorchimagenetrightfitoncegpu"
read_or_write="write" section="cost" >}}
+
+## How has various metrics changed when running the pipeline for different
Beam SDK versions?
+
+{{< performance_looks io="pytorchimagenetrightfitoncegpu"
read_or_write="write" section="version" >}}
+
+## How has various metrics changed over time when running the pipeline?
+
+{{< performance_looks io="pytorchimagenetrightfitoncegpu"
read_or_write="write" section="date" >}}
diff --git
a/website/www/site/content/en/performance/pytorchimageobjectdetectionbatchcpu/_index.md
b/website/www/site/content/en/performance/pytorchimageobjectdetectionbatchcpu/_index.md
new file mode 100644
index 00000000000..a2ef39a2cf3
--- /dev/null
+++
b/website/www/site/content/en/performance/pytorchimageobjectdetectionbatchcpu/_index.md
@@ -0,0 +1,41 @@
+---
+title: "PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch CPU
Performance"
+---
+
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch CPU
+
+**Model**: PyTorch Image Object Detection — Faster R-CNN ResNet-50 FPN
(pretrained on COCO)
+**Accelerator**: CPU only
+**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM)
+
+This batch pipeline performs object detection using an open-source PyTorch
Faster R-CNN ResNet-50 FPN model on CPU.
+It reads image URIs from GCS, decodes and preprocesses images, and runs
batched inference with a fixed batch size.
+
+The following graphs show various metrics when running PyTorch Image Object
Detection Faster R-CNN ResNet-50 Batch CPU pipeline.
+See the [glossary](/performance/glossary) for definitions.
+
+Full pipeline implementation is available
[here](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/inference/pytorch_image_object_detection.py).
+
+## What is the estimated cost to run the pipeline?
+
+{{< performance_looks io="pytorchimageobjectdetectionbatchcpu"
read_or_write="write" section="cost" >}}
+
+## How has various metrics changed when running the pipeline for different
Beam SDK versions?
+
+{{< performance_looks io="pytorchimageobjectdetectionbatchcpu"
read_or_write="write" section="version" >}}
+
+## How has various metrics changed over time when running the pipeline?
+
+{{< performance_looks io="pytorchimageobjectdetectionbatchcpu"
read_or_write="write" section="date" >}}
diff --git
a/website/www/site/content/en/performance/pytorchimageobjectdetectionbatchgpu/_index.md
b/website/www/site/content/en/performance/pytorchimageobjectdetectionbatchgpu/_index.md
new file mode 100644
index 00000000000..f6d6107d19f
--- /dev/null
+++
b/website/www/site/content/en/performance/pytorchimageobjectdetectionbatchgpu/_index.md
@@ -0,0 +1,41 @@
+---
+title: "PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch GPU
Performance"
+---
+
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# PyTorch Image Object Detection Faster R-CNN ResNet-50 Batch GPU
+
+**Model**: PyTorch Image Object Detection — Faster R-CNN ResNet-50 FPN
(pretrained on COCO)
+**Accelerator**: Tesla T4 GPU (fixed batch size)
+**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM)
+
+This batch pipeline performs object detection using an open-source PyTorch
Faster R-CNN ResNet-50 FPN model on GPU.
+It reads image URIs from GCS, decodes and preprocesses images, and runs
batched inference with a fixed batch size to measure stable GPU performance.
+
+The following graphs show various metrics when running PyTorch Image Object
Detection Faster R-CNN ResNet-50 Batch GPU pipeline.
+See the [glossary](/performance/glossary) for definitions.
+
+Full pipeline implementation is available
[here](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/inference/pytorch_image_object_detection.py).
+
+## What is the estimated cost to run the pipeline?
+
+{{< performance_looks io="pytorchimageobjectdetectionbatchgpu"
read_or_write="write" section="cost" >}}
+
+## How has various metrics changed when running the pipeline for different
Beam SDK versions?
+
+{{< performance_looks io="pytorchimageobjectdetectionbatchgpu"
read_or_write="write" section="version" >}}
+
+## How has various metrics changed over time when running the pipeline?
+
+{{< performance_looks io="pytorchimageobjectdetectionbatchgpu"
read_or_write="write" section="date" >}}
diff --git
a/website/www/site/content/en/performance/pytorchimageobjectdetectionstreamingcpu/_index.md
b/website/www/site/content/en/performance/pytorchimageobjectdetectionstreamingcpu/_index.md
new file mode 100644
index 00000000000..b866d2893cd
--- /dev/null
+++
b/website/www/site/content/en/performance/pytorchimageobjectdetectionstreamingcpu/_index.md
@@ -0,0 +1,41 @@
+---
+title: "PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming CPU
Performance"
+---
+
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming CPU
+
+**Model**: PyTorch Image Object Detection — Faster R-CNN ResNet-50 FPN
(pretrained on COCO)
+**Accelerator**: CPU only
+**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM)
+
+This streaming pipeline performs object detection using an open-source PyTorch
Faster R-CNN ResNet-50 FPN model on CPU.
+It reads image URIs from GCS, decodes and preprocesses images, and runs
inference.
+
+The following graphs show various metrics when running PyTorch Image Object
Detection Faster R-CNN ResNet-50 Streaming CPU pipeline.
+See the [glossary](/performance/glossary) for definitions.
+
+Full pipeline implementation is available
[here](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/inference/pytorch_image_object_detection.py).
+
+## What is the estimated cost to run the pipeline?
+
+{{< performance_looks io="pytorchimageobjectdetectionstreamingcpu"
read_or_write="write" section="cost" >}}
+
+## How has various metrics changed when running the pipeline for different
Beam SDK versions?
+
+{{< performance_looks io="pytorchimageobjectdetectionstreamingcpu"
read_or_write="write" section="version" >}}
+
+## How has various metrics changed over time when running the pipeline?
+
+{{< performance_looks io="pytorchimageobjectdetectionstreamingcpu"
read_or_write="write" section="date" >}}
diff --git
a/website/www/site/content/en/performance/pytorchimageobjectdetectionstreaminggpu/_index.md
b/website/www/site/content/en/performance/pytorchimageobjectdetectionstreaminggpu/_index.md
new file mode 100644
index 00000000000..443f5e99e76
--- /dev/null
+++
b/website/www/site/content/en/performance/pytorchimageobjectdetectionstreaminggpu/_index.md
@@ -0,0 +1,41 @@
+---
+title: "PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming GPU
Performance"
+---
+
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# PyTorch Image Object Detection Faster R-CNN ResNet-50 Streaming GPU
+
+**Model**: PyTorch Image Object Detection — Faster R-CNN ResNet-50 FPN
(pretrained on COCO)
+**Accelerator**: Tesla T4 GPU
+**Host**: 50 × n1-standard-4 (4 vCPUs, 15 GB RAM)
+
+This streaming pipeline performs object detection using an open-source PyTorch
Faster R-CNN ResNet-50 FPN model on GPU.
+It reads image URIs from GCS, decodes and preprocesses images, and runs
inference.
+
+The following graphs show various metrics when running PyTorch Image Object
Detection Faster R-CNN ResNet-50 Streaming GPU pipeline.
+See the [glossary](/performance/glossary) for definitions.
+
+Full pipeline implementation is available
[here](https://github.com/apache/beam/blob/master/sdks/python/apache_beam/examples/inference/pytorch_image_object_detection.py).
+
+## What is the estimated cost to run the pipeline?
+
+{{< performance_looks io="pytorchimageobjectdetectionstreaminggpu"
read_or_write="write" section="cost" >}}
+
+## How has various metrics changed when running the pipeline for different
Beam SDK versions?
+
+{{< performance_looks io="pytorchimageobjectdetectionstreaminggpu"
read_or_write="write" section="version" >}}
+
+## How has various metrics changed over time when running the pipeline?
+
+{{< performance_looks io="pytorchimageobjectdetectionstreaminggpu"
read_or_write="write" section="date" >}}
diff --git a/website/www/site/data/performance.yaml
b/website/www/site/data/performance.yaml
index c27dcc22f23..f0292fab747 100644
--- a/website/www/site/data/performance.yaml
+++ b/website/www/site/data/performance.yaml
@@ -283,21 +283,197 @@ looks:
title: AvgThroughputBytesPerSec by Version
- id: P7wKZy6tQFWbbDfm4HzfCJnsQrVgfGsJ
title: AvgThroughputElementsPerSec by Version
- mltransformvocab:
+ pytorchimagenetrightfitcpu:
write:
- folder: 107
+ folder: 92
cost:
- - id: CccNCHQ23Js2tmxTDYVnjxwfx2zSFSjY
+ - id: zJhxrMmxJ3zVHH5WZnDQqcBHdFDrBhxK
title: RunTime and EstimatedCost
date:
- - id: n7Gn7c7KSW52ZgXCQXndHgWtjVPK8W33
+ - id: RybzxZdkXJg6PzQZkBcfxTJkByTf3ZV5
title: AvgThroughputBytesPerSec by Date
- - id: 3tcg6cSh6tBg6FmpTxgzyMtvFqWKkQn3
+ - id: xTVYPytQVH7zXYz7SvphnRV4nQcxCddp
title: AvgThroughputElementsPerSec by Date
version:
- - id: tPYwJngPDBsKjK3DNgGHGsCyTpxdfmTB
+ - id: dGN6Zr6rh7DfnRtTDCN6GHcNfhSkrbCq
title: AvgThroughputBytesPerSec by Version
- - id: cC75NnCbQT3mQmKVHtDxzptXpwPb64qz
+ - id: VJMWrZh3jXk2mqCZk4NQn3tBrHgGWqnC
+ title: AvgThroughputElementsPerSec by Version
+ pytorchimagenetrightfitoncecpu:
+ write:
+ folder: 97
+ cost:
+ - id: DhsvrRxpby2F25jbj8NPVR6FhzKgsHBP
+ title: RunTime and EstimatedCost
+ date:
+ - id: sntwpmZjMVT57kvG8KM8rzDjBKsrRTF7
+ title: AvgThroughputBytesPerSec by Date
+ - id: 5GJHCVH65y2zmnrZ3QqmYqk8vKfPD4D6
+ title: AvgThroughputElementsPerSec by Date
+ version:
+ - id: rz24FQ24rz3HtFmpzK2rKD4zFT5kpG46
+ title: AvgThroughputBytesPerSec by Version
+ - id: jZkcSdHbvmGXy8f37RShNvgGv6HVN7Dy
+ title: AvgThroughputElementsPerSec by Version
+ pytorchimagenetrightfitgpu:
+ write:
+ folder: 98
+ cost:
+ - id: sMgzq4N8kJfjSjhSzVDzmXSHbXKwCy4r
+ title: RunTime and EstimatedCost
+ date:
+ - id: t3D6hqq7BywmvjBmxtWCvRMsFFMgPBg7
+ title: AvgThroughputBytesPerSec by Date
+ - id: MRptKCFsnTKcxPgVKKVvs7c8XVTf6MSN
+ title: AvgThroughputElementsPerSec by Date
+ version:
+ - id: v6jHqBc8WvCPspgdxQdjSZ6bCgqqPvZ3
+ title: AvgThroughputBytesPerSec by Version
+ - id: DhGNQDmRDhYSCDgXDsx6BtCcDfwKCWhb
+ title: AvgThroughputElementsPerSec by Version
+ pytorchimagenetrightfitoncegpu:
+ write:
+ folder: 99
+ cost:
+ - id: YbwBRmMVBCxPzjm22vWvJ2fDG5mmM3BR
+ title: RunTime and EstimatedCost
+ date:
+ - id: WtCZn2CrRrDdygbxQhfVhNM8F7NRRpsW
+ title: AvgThroughputBytesPerSec by Date
+ - id: RBMNkqksbcN3WcwCxz4B5mxVq5Y8yk2x
+ title: AvgThroughputElementsPerSec by Date
+ version:
+ - id: 48tzWvydG73Y3QwPJwQzYCkzdDnpF2T7
+ title: AvgThroughputBytesPerSec by Version
+ - id: 3VDCrNY6q8FyS28YRrxjJtMBYkxKRdBw
+ title: AvgThroughputElementsPerSec by Version
+ pytorchimageobjectdetectionbatchgpu:
+ write:
+ folder: 93
+ cost:
+ - id: XGPVSYhVbZGJHQCtMPW4nRGynxXpdzdh
+ title: RunTime and EstimatedCost
+ date:
+ - id: ZW48KBPBxShGgWx53vjfvgqp6cVmHCNB
+ title: AvgThroughputBytesPerSec by Date
+ - id: BjMWg26F3gNHQyZSsg8HjhTg3mCh6jFJ
+ title: AvgThroughputElementsPerSec by Date
+ version:
+ - id: kbF7Gnqjsjnvh3MKvYszSgWFbgDYYTWR
+ title: AvgThroughputBytesPerSec by Version
+ - id: cTxrpY3KGrCb35dq7fnjvdsDnd7t85pJ
+ title: AvgThroughputElementsPerSec by Version
+ pytorchimageobjectdetectionbatchcpu:
+ write:
+ folder: 100
+ cost:
+ - id: QbMVTdnxzmZzfymSrKsGTvyqFTMpwqy7
+ title: RunTime and EstimatedCost
+ date:
+ - id: v6H8sqn7X66j5g6qpgC6B57z624cCfDM
+ title: AvgThroughputBytesPerSec by Date
+ - id: KBMSpWzB5RgdYsTdZxGCXDYbWpskVqGS
+ title: AvgThroughputElementsPerSec by Date
+ version:
+ - id: GWz5VDrMTxSCrFqQwk2MyPHMwBHC253c
+ title: AvgThroughputBytesPerSec by Version
+ - id: NBWHNG8GmGHp8Bmg25wqg3tP8jWdP5PW
+ title: AvgThroughputElementsPerSec by Version
+ pytorchimageobjectdetectionstreaminggpu:
+ write:
+ folder: 101
+ cost:
+ - id: SfgfDsbRVk67XMnKZShq3xVBFghN7GSv
+ title: RunTime and EstimatedCost
+ date:
+ - id: MtMJZTTrkgXZz24zbQbpzwYNjWwcfhqw
+ title: AvgThroughputBytesPerSec by Date
+ - id: kKncXDm5MKvfwVzFyXqQ4TpfR5VdT5z7
+ title: AvgThroughputElementsPerSec by Date
+ version:
+ - id: ZWQ7vnsXYb5PDr5yDFjM3QP8gkkmdgdQ
+ title: AvgThroughputBytesPerSec by Version
+ - id: cTmDCbdCQyrsK3fS333vmR3BwCwrGZgp
+ title: AvgThroughputElementsPerSec by Version
+ pytorchimageobjectdetectionstreamingcpu:
+ write:
+ folder: 102
+ cost:
+ - id: FHgY6JfxRc23jJ6THkb7sPVw2HSw4kgs
+ title: RunTime and EstimatedCost
+ date:
+ - id: G5DNH6vXpGVnYP8ZGMwppdGvgvndM8kn
+ title: AvgThroughputBytesPerSec by Date
+ - id: CGR43NrksWDscGNWCKsVGCwc3RxmvGhD
+ title: AvgThroughputElementsPerSec by Date
+ version:
+ - id: wGGVRGxBjXxQf3yvynSFtcDCVCqKzByy
+ title: AvgThroughputBytesPerSec by Version
+ - id: M5nWYF76fWPnbTH6QkzWqzRV8fSWxtWD
+ title: AvgThroughputElementsPerSec by Version
+ pytorchimagecaptioningbatchgpu:
+ write:
+ folder: 94
+ cost:
+ - id: nhYggnCYJzfgDyTXZRGJwpSyYKvBpfhV
+ title: RunTime and EstimatedCost
+ date:
+ - id: qzjkYQJCrxjc8GPcBF7dFJSFdTXQKxmR
+ title: AvgThroughputBytesPerSec by Date
+ - id: kgkdbBCbkhDHpb2rHNGQDxcwGqzykwqD
+ title: AvgThroughputElementsPerSec by Date
+ version:
+ - id: 4p7b4HWVMRC8HXDZYMXbVYf2VPzYQ2kz
+ title: AvgThroughputBytesPerSec by Version
+ - id: xQZRDYJRvmV7qMhb3j2XTrPmC8TP4DTr
+ title: AvgThroughputElementsPerSec by Version
+ pytorchimagecaptioningbatchcpu:
+ write:
+ folder: 103
+ cost:
+ - id: M4vTdbrK6BxJXdXcnR7WPwW7VF4Fmhbb
+ title: RunTime and EstimatedCost
+ date:
+ - id: T23Fty8X6dF6DFhxSy32ymSwqz64fPXT
+ title: AvgThroughputBytesPerSec by Date
+ - id: Wm2qdmKgHbzfJbW7vNVfrGtQWV4xGCx3
+ title: AvgThroughputElementsPerSec by Date
+ version:
+ - id: wnQqxHgwJstnV4fPCkNpvMxJpyPG4326
+ title: AvgThroughputBytesPerSec by Version
+ - id: kPMV7d8Htpp6CT8q5hC6NF5DbTRwBQcJ
+ title: AvgThroughputElementsPerSec by Version
+ pytorchimagecaptioningstreaminggpu:
+ write:
+ folder: 104
+ cost:
+ - id: rgrPw4ZdZjCD8cvKqdvdPGGtPJgWfNK5
+ title: RunTime and EstimatedCost
+ date:
+ - id: jwpm4S36mGXPrY2nfpWrCvDHN6Nsrnpq
+ title: AvgThroughputBytesPerSec by Date
+ - id: QmY5QVTpndNTBJdqWjMxxD4zQkZqn6Xs
+ title: AvgThroughputElementsPerSec by Date
+ version:
+ - id: htD3YwWcTPYcRk4wqtqHshnt7hrgyrfk
+ title: AvgThroughputBytesPerSec by Version
+ - id: rv3QbsbxBV68MWG8hdjhnH84zJsPgJQh
+ title: AvgThroughputElementsPerSec by Version
+ pytorchimagecaptioningstreamingcpu:
+ write:
+ folder: 105
+ cost:
+ - id: YkFKPzV2Rvgy5hmmx2KPyJCHhbxhSjzj
+ title: RunTime and EstimatedCost
+ date:
+ - id: 5WStCthVw8gtCdKrY5mhQf8T6N45GGqH
+ title: AvgThroughputBytesPerSec by Date
+ - id: ZnJj4QFrd3dwmsmv3xdNKytPfbW7j88C
+ title: AvgThroughputElementsPerSec by Date
+ version:
+ - id: VHqjNCVNScFqHF8d65khMDGxfSPrBrMh
+ title: AvgThroughputBytesPerSec by Version
+ - id: ykZJhrhHDzyPBSTxyNcdYG7yw5qs3dYj
title: AvgThroughputElementsPerSec by Version
mltransformonehot:
write:
@@ -315,6 +491,22 @@ looks:
title: AvgThroughputBytesPerSec by Version
- id: Cn5FsXkdy2ZXCxCJshSCxcjsTW3TXf3c
title: AvgThroughputElementsPerSec by Version
+ mltransformvocab:
+ write:
+ folder: 107
+ cost:
+ - id: CccNCHQ23Js2tmxTDYVnjxwfx2zSFSjY
+ title: RunTime and EstimatedCost
+ date:
+ - id: n7Gn7c7KSW52ZgXCQXndHgWtjVPK8W33
+ title: AvgThroughputBytesPerSec by Date
+ - id: 3tcg6cSh6tBg6FmpTxgzyMtvFqWKkQn3
+ title: AvgThroughputElementsPerSec by Date
+ version:
+ - id: tPYwJngPDBsKjK3DNgGHGsCyTpxdfmTB
+ title: AvgThroughputBytesPerSec by Version
+ - id: cC75NnCbQT3mQmKVHtDxzptXpwPb64qz
+ title: AvgThroughputElementsPerSec by Version
mltransform-text-embedding:
write:
folder: 109