PyNvVideoCodec Performance#

PyNvVideoCodec offers video encode and decode performance close to Video Codec SDK. This chapter outlines the performance capabilities enabled by unique APIs and features of PyNvVideoCodec.

Note

The benchmarks presented in this chapter use the BtBN FFmpeg build for comparison purposes.

Benchmark Overview#

The benchmark scripts provided with PyNvVideoCodec measure performance across different use cases. Each benchmark automatically generates test videos using FFmpeg on the first run, and subsequent runs will reuse these videos for consistent testing.

Important Considerations Before Running Benchmarks:

  • Initial run time: The first execution of any benchmark script takes significantly longer because it generates sample videos using FFmpeg. Subsequent runs are much faster as they reuse the generated videos.

  • Disk space: The generated test videos are stored locally. Ensure sufficient disk space is available.

  • GPU requirements: A CUDA-capable NVIDIA GPU with NVDEC hardware decoder support is required.

Understanding the NVDEC Parameter#

Benchmark scripts require an --nvdecs parameter, which specifies the number of hardware NVDEC (NVIDIA Video Decoder) instances available on your GPU. This parameter is critical for achieving optimal performance.

How to determine your NVDEC count:

  1. Visit the NVIDIA Video Encode and Decode GPU Support Matrix

  2. Find your GPU model in the list

  3. Look for the “NVDEC” column to see the number of decoder instances

Common NVDEC counts by GPU:

  • NVIDIA L40G: 3 NVDECs

  • NVIDIA A100: 5 NVDECs

  • NVIDIA RTX 4090: 2 NVDECs

  • NVIDIA RTX 3090: 1 NVDEC

  • NVIDIA T4: 2 NVDECs

Setting the correct NVDEC count allows the benchmark to spawn the appropriate number of threads to fully saturate the available hardware decoders, maximizing throughput.

Benchmark Dependencies#

Before running the benchmark scripts, ensure you have all required Python packages installed. requirements.txt file is provided in the benchmark scripts directory.

Install the dependencies using:

pip install -r requirements.txt

Additional requirements:

  • FFmpeg: Must be installed and accessible in your system PATH. The benchmarks use FFmpeg (with NVENC support) to generate test videos. We recommend using the BtBN FFmpeg builds which include NVIDIA hardware acceleration support.

  • CUDA Toolkit: A compatible CUDA toolkit must be installed for PyCUDA.

Expected Execution Time#

The following table provides approximate execution times for each benchmark script. These times were measured on an NVIDIA L40G GPU with 3 threads (matching the 3 NVDECs available).

Table 3 Benchmark Execution Time Estimates (L40G, 3 Threads)#

Benchmark Script

Execution Time

Notes

frame_sampling_benchmark.py

~1 minute

Tests 1080p videos with different GOP sizes

cached_decoder_benchmark.py

~42 minutes

Tests multiple resolutions (360p to 4K) with 500 iterations each

segmented_transcode_benchmark.py

~6 minutes

Generates and processes video segments

Actual execution times will vary depending on your GPU model, CPU, storage speed, and the number of threads used.

Available Benchmarks#

Frame Retrieval#

Performance benchmarks for different frame retrieval patterns using PyNvVideoCodec decoder.

Objective#

This benchmark measures the sampling performance of PyNvVideoCodec when retrieving frames using different access patterns. It evaluates how efficiently frames can be extracted from a video depending on whether you need sequential, uniformly distributed, or randomly selected frames.

What this benchmark measures:

  • Frame retrieval throughput (Frames Per Second) for three sampling patterns

  • Impact of GOP (Group of Pictures) size on seek performance

  • Efficiency of direct frame sampling versus sequential decoding

  • Multi-threaded scaling performance across available NVDECs

Sampling Patterns Tested:

  • Sequential Decoding: Retrieves frames in order from the start of the video (e.g., first 100 frames). This is the fastest pattern as it requires minimal seeking.

  • Uniform Sampling: Retrieves frames at regular intervals across the entire video duration. For example, sampling 30 frames from a 30-second video fetches one frame every second.

  • Random Sampling: Retrieves frames at randomly selected positions throughout the video. This pattern represents the most challenging access pattern due to unpredictable seek locations. The script uses torch.randperm() to generate unique random frame indices, ensuring no duplicate frames are sampled.

Key Performance Indicators (KPI):

  • FPS (Frames Per Second): The number of frames retrieved per second. Higher is better.

  • Efficiency: Ratio comparing direct sampling performance to sequential decode-then-sample approach. Values greater than 1.0x indicate direct sampling is faster than decoding all frames and then selecting the needed ones.

How the Benchmark Works#

The benchmark follows these steps:

  1. Video Generation (first run only): Creates test videos using FFmpeg with the mandelbrot pattern at 1080p resolution. Multiple videos with different GOP sizes (default: 30 and 250) are generated to test the impact of GOP on seek performance.

  2. Thread Setup: Creates multiple decoder threads (1 thread for single-threaded test, N threads to match NVDEC count).

  3. Sequential Decode Test: Each thread decodes the first N frames (default: 100) sequentially and measures FPS.

  4. Uniform Sampling Test: Each thread samples M frames (default: 30) at regular intervals and measures FPS. The efficiency is calculated by comparing against the time needed to sequentially decode up to the last sampled frame.

  5. Random Sampling Test: Each thread samples M frames at random positions and measures FPS, also calculating efficiency.

  6. Results Aggregation: FPS and efficiency metrics are calculated and displayed for all configurations.

Running the Benchmark#

Basic Usage:

python frame_sampling_benchmark.py --nvdecs 3

Replace 3 with the number of NVDEC instances on your GPU. See the NVDEC Parameter section to determine your GPU’s NVDEC count.

Command Line Options:

Option

Default

Description

--nvdecs

(required)

Number of NVDEC instances on your GPU. Determines the number of parallel decoder threads.

--resolution, -res

1920x1080

Video resolution for generated test videos

--gop, -g

30 250

GOP sizes to test (space-separated list)

--duration, -d

30

Video duration in seconds

--fps, -f

30

Video frames per second

--num-seq-frames, -seq

100

Number of frames to decode for sequential test

--num-samp-frames, -samp

30

Number of frames to sample for uniform/random tests

--verbose, -v

False

Show detailed per-thread performance information

Example Commands:

# Run benchmark with default settings on a GPU with 3 NVDECs
python frame_sampling_benchmark.py --nvdecs 3

# Run with 720p resolution and specific GOP sizes
python frame_sampling_benchmark.py --nvdecs 3 --resolution 1280x720 --gop 30 60 120

# Run with verbose output showing per-thread details
python frame_sampling_benchmark.py --nvdecs 3 --verbose

# Run with custom sampling parameters
python frame_sampling_benchmark.py --nvdecs 2 --num-seq-frames 200 --num-samp-frames 50

Output Files:

  • benchmark_results.json - Detailed results including system info and per-test metrics

  • benchmark_videos/ - Generated test videos (reused in subsequent runs)

Expected Execution Time: Approximately 1 minute on an L40G GPU with 3 threads.

Benchmark Environment#

Environment:

  • GPU: 1 x L40G (3 NVDECs)

  • CPU: AMD EPYC 7313P 16-Core Processor, 2 threads per core

  • OS: Ubuntu 22.04

Methodology#

  • Script to execute benchmark: frame_sampling_benchmark.py

  • Dataset generated using FFmpeg with the following default parameters:

    • Resolution: 1920x1080

    • GOP: 30 & 250

    • Duration: 30 seconds

    • Frame Rate: 30

  • Multithreaded implementation to fully utilize NVDECs (multiple Python threads)

  • Each Python thread independently decodes the same video & reports the FPS

Benchmark Results#

The following results were obtained on the representative platform described in the Benchmark Environment section above. You can run the benchmark script in your own environment to obtain results specific to your hardware configuration.

Sequential Decode (First 100 Frames)

Decodes frames in sequential order from the start of the video. This approach retrieves a specified number of consecutive frames (e.g., first 100 frames).

Table 4 Sequential Decode Performance#

Video Config

Num Threads

FPS

1920x1080 250gop 30s

1

886

1920x1080 250gop 30s

3

2615.4

1920x1080 30gop 30s

1

881.1

1920x1080 30gop 30s

3

2609.4

Random Sampling (30 Frames)

Randomly selects frames from across the entire video duration. This method is useful for obtaining a representative sample of frames throughout the video.

Table 5 Random Sampling Performance#

Video Config

Num Threads

FPS

Efficiency

1920x1080 250gop 30s

1

37.3

1.02x

1920x1080 250gop 30s

3

110.8

1.03x

1920x1080 30gop 30s

1

78.4

2.14x

1920x1080 30gop 30s

3

218

1.98x

Uniform Sampling (30 Frames)

Evenly distributes frame sampling across the entire video duration. For example, when sampling 30 frames from a 30-second video, it fetches one frame every second.

Table 6 Uniform Sampling Performance#

Video Config

Num Threads

FPS

Efficiency

1920x1080 250gop 30s

1

39.6

1.05x

1920x1080 250gop 30s

3

117.6

1.05x

1920x1080 30gop 30s

1

54.2

1.44x

1920x1080 30gop 30s

3

158.4

1.42x

Note on Efficiency: Efficiency represents the performance comparison between two approaches:

  1. Direct sampling: Decoding specific frames directly using seek operations

  2. Sequential decode + sampling: Decoding all frames sequentially up to the last required frame, then extracting the needed frames

The efficiency value shows how much faster direct sampling is compared to sequential decoding with sampling. Higher efficiency values indicate better performance of the direct sampling approach.

Important: Efficiency should only be compared within the same thread configuration. Do not compare efficiency values across different thread counts. For example, while 1-thread random sampling shows 2.14x efficiency and 3-thread shows 1.98x efficiency, this does not mean single-threaded is better. The 3-thread configuration achieves 218 FPS compared to 78.4 FPS for single-thread-a 2.8x improvement in absolute throughput. The efficiency metric only indicates how much faster direct sampling is versus sequential decoding within that same thread configuration.

Key Observations#

  • GOP size has significant impact on frame retrieval performance:

    • For random sampling, smaller GOP size (30) increases performance by 110% as compared to bigger GOP size (250)

    • For uniform sampling, smaller GOP size (30) increases performance by 37% as compared to bigger GOP size (250)

    • Sequential decoding performance is largely unaffected by GOP size

  • Multi-threading provides significant absolute performance gains:

    • Sequential decoding: 881 FPS (1 thread) → 2609 FPS (3 threads) = 2.96x speedup

    • Random sampling (30 GOP): 78.4 FPS (1 thread) → 218 FPS (3 threads) = 2.78x speedup

    • Uniform sampling (30 GOP): 54.2 FPS (1 thread) → 158.4 FPS (3 threads) = 2.92x speedup

  • Efficiency comparison (within same thread configuration):

    • Smaller GOP (30) provides higher efficiency for both sampling methods because less data needs to be decoded to reach each target frame

    • Random sampling with 30 GOP: 2.14x efficiency (1 thread), 1.98x efficiency (3 threads)

    • Uniform sampling with 30 GOP: 1.44x efficiency (1 thread), 1.42x efficiency (3 threads)

    • Larger GOP (250) shows minimal efficiency advantage (1.02x-1.05x) because more frames must be decoded to reach seek points

Decoder Reuse#

Performance benefits of reusing decoder instances when processing multiple videos.

Objective#

This benchmark measures and compares the performance of NVIDIA’s video decoder in two operational modes:

  1. Simple Decoder: Creates a new decoder instance for each video file

  2. Cached Decoder: Reuses the same decoder instance across multiple video files through reconfiguration

What this benchmark measures:

  • Decoding throughput (Frames Per Second) for both decoder modes

  • Total time taken to decode a batch of video clips

  • Performance comparison across different video resolutions (360p, 480p, 720p, 1080p, 4K)

  • Impact of decoder initialization overhead on overall performance

Key Performance Indicator (KPI): The primary metric is FPS (Frames Per Second). Higher FPS indicates better decoder efficiency. The speedup ratio (Cached FPS / Simple FPS) shows the benefit of decoder caching.

How the Benchmark Works#

The benchmark follows these steps:

  1. Video Generation (first run only): Creates test videos using FFmpeg with the mandelbrot test pattern at various resolutions (360p, 480p, 720p, 1080p, 4K). Each video is 2 seconds long at 30 fps.

  2. Workload Creation: Each generated video is queued 500 times to create sufficient workload to saturate the GPU’s NVDEC hardware.

  3. Thread Distribution: Videos are distributed across multiple decoder threads (1 thread for single-threaded test, N threads to match NVDEC count).

  4. Simple Decoder Test: Each thread creates a new decoder instance for every video clip and measures total decoding time.

  5. Cached Decoder Test: Each thread creates a single decoder instance with caching enabled and reconfigures it for each subsequent video, measuring total decoding time.

  6. Results Comparison: FPS is calculated for both modes and compared across all resolutions.

Running the Benchmark#

Basic Usage:

python cached_decoder_benchmark.py --nvdecs 3

Replace 3 with the number of NVDEC instances on your GPU. See the NVDEC Parameter section to determine your GPU’s NVDEC count.

Command Line Options:

Option

Default

Description

--nvdecs

(required)

Number of NVDEC instances on your GPU. This determines the number of parallel decoder threads.

--codec

h264

Video codec to use: h264, hevc, or av1

--fps

30

Frame rate for generated test videos

--gop

60

GOP (Group of Pictures) size for generated videos

--plot-only

False

Skip benchmark and only generate plots from existing JSON results. The JSON files from the existing runs are stored in the same directory as the benchmark script.

Example Commands:

# Run benchmark with H.264 codec on a GPU with 3 NVDECs
python cached_decoder_benchmark.py --nvdecs 3

# Run benchmark with HEVC codec on a GPU with 2 NVDECs
python cached_decoder_benchmark.py --nvdecs 2 --codec hevc

# Run benchmark with custom video settings
python cached_decoder_benchmark.py --nvdecs 4 --codec av1 --fps 60 --gop 120

# Only generate plots from existing results
python cached_decoder_benchmark.py --nvdecs 3 --plot-only

Output Files:

  • cached_decoder_performance_{codec}_{threads}_threads.json - Detailed results in JSON format

  • cached_decoder_performance_{codec}_{threads}_threads.png - Performance comparison bar graphs

  • test_videos_{codec}/ - Generated test videos (reused in subsequent runs)

Expected Execution Time: Approximately 42 minutes on an L40G GPU with 3 threads. This benchmark takes longer because it tests multiple resolutions (360p to 4K) with 500 iterations each to ensure statistically significant results.

Benchmark Environment#

Environment:

  • GPU: 1 x L40G (3 NVDECs)

  • CPU: AMD EPYC 7313P 16-Core Processor, 2 threads per core

  • OS: Ubuntu 22.04

Methodology#

  • Script to execute benchmark: cached_decoder_benchmark.py

  • Dataset generated using FFmpeg with the following parameters:

    • Resolutions: 360p, 480p, 720p, 1080p, 4k

    • Frame Rate: 30 fps

    • GOP Size: 60

    • Duration: 2 seconds (short) and 30 seconds (long)

    • Pattern: mandelbrot

  • 5 videos created using FFmpeg (1 video per resolution)

  • Each video was reused 500 times to create enough decoding workload to fully saturate all available NVDEC hardware instances.

  • Videos are distributed across multiple decoder threads

  • Example configuration: In a 20-clip/4-thread setup, each thread processes 5 videos

Decoder Types:

  • Simple decoder:

    • Creates a new decoder instance for each video clip

    • For example, if a thread has to decode 5 videos, a total of 5 decoder instances will be created

  • Cached decoder:

    • Creates a single decoder instance per thread

    • Reuses the same decoder for subsequent clips through reconfiguration

    • Implementation follows the principles outlined in Decoder Caching

    • For example, for 5 videos per thread, only one decoder instance is created and reused

Benchmark Results#

The following results were obtained on the representative platform described in the Benchmark Environment section above. You can run the benchmark script in your own environment to obtain results specific to your hardware configuration.

Short Duration Videos (2 seconds)

Performance comparison when decoding many short video clips, where decoder initialization overhead is most significant.

Table 7 Decoder Reuse Performance - Short Videos (2 sec)#

Resolution

Decoder Type

Time Taken (s)

FPS

360p

Simple

17.05

1760

360p

Cached

2.37

12679

480p

Simple

17.27

1737

480p

Cached

3.28

9151

720p

Simple

18.55

1617

720p

Cached

5.78

5190

1080p

Simple

20.53

1461

1080p

Cached

11.53

2602

4k

Simple

53.28

563

4k

Cached

42.78

701

Long Duration Videos (30 seconds)

Performance comparison when decoding longer video clips, where actual decoding time dominates over initialization overhead.

Table 8 Decoder Reuse Performance - Long Videos (30 sec)#

Resolution

Decoder Type

Time Taken (s)

FPS

360p

Simple

39.28

11456

360p

Cached

30.78

14621

480p

Simple

54.28

8291

480p

Cached

44.78

10049

720p

Simple

94.03

4786

720p

Cached

84.28

5339

1080p

Simple

188.28

2390

1080p

Cached

177.78

2531

4k

Simple

709.78

634

4k

Cached

695.53

647

Bar chart comparing performance of simple decoder creation vs. cached decoder approach across resolutions

Figure 2 Performance Comparison: Simple vs. Cached Decoders Bar chart comparing performance of simple decoder creation vs. cached decoder approach across resolutions for short duration videos#

Key Observations:

  • Cached decoders consistently outperform simple decoders across all resolutions and video durations

  • For short videos (2 sec), performance improvement is dramatic at lower resolutions:

    • 360p: 7.2x faster (12679 vs 1760 FPS)

    • 480p: 5.3x faster (9151 vs 1737 FPS)

    • 720p: 3.2x faster (5190 vs 1617 FPS)

    • 1080p: 1.8x faster (2602 vs 1461 FPS)

    • 4K: 1.2x faster (701 vs 563 FPS)

  • For long videos (30 sec), the improvement is more modest as decoding time dominates:

    • 360p: 1.3x faster

    • 480p: 1.2x faster

    • 720p-4K: 1.02x-1.1x faster

  • The performance benefit comes from eliminating decoder initialization overhead, which is most significant when processing many short video clips

Segmented Transcoding#

Performance comparison of PyNvVideoCodec’s segmented transcoding approach against traditional FFmpeg-based methods.

Objective#

This benchmark compares the performance of different approaches for transcoding video segments. It measures how efficiently PyNvVideoCodec’s Transcoder class handles segmented video transcoding compared to traditional FFmpeg-based methods.

What this benchmark measures:

  • Transcoding throughput (Frames Per Second) for each method

  • Total processing time for a batch of video segments

  • Performance difference between PyNvVideoCodec and FFmpeg approaches

  • Impact of different FFmpeg configurations (with/without filter_complex, audio handling)

Transcoding Methods Compared:

  • Mode 0 - PyNvVideoCodec Transcoding: Uses PyNvVideoCodec’s Transcoder class with segmented_transcode() method. Maintains persistent GPU context and avoids repeated encoder/decoder initialization.

  • Mode 1 - FFmpeg Without Map: Uses separate FFmpeg commands for each segment with -ss/-to for time ranges. Simple approach but spawns multiple processes.

  • Mode 2 - FFmpeg With Map (No Audio): Uses FFmpeg’s filter_complex to process multiple segments in one command. Video only processing.

  • Mode 3 - FFmpeg With Map (With Audio): Same as Mode 2 but includes audio stream processing.

Key Performance Indicator (KPI): The primary metric is FPS (Frames Per Second) representing transcoding throughput. Higher FPS indicates faster processing. The speedup ratio (PyNvVideoCodec FPS / FFmpeg FPS) shows the performance advantage of using PyNvVideoCodec.

How the Benchmark Works#

The benchmark follows these steps:

  1. Video Generation (first run only): Creates a test video using FFmpeg with the mandelbrot pattern and audio. The video includes both H.264 video and AAC audio tracks. A short base clip is generated and then looped to reach the target duration.

  2. Segment Creation: Generates random non-overlapping segments within the video. Each segment has a configurable minimum duration (default: 5 seconds).

  3. PyNvVideoCodec Transcoding Test: Uses PyNvVideoCodec’s Transcoder class to transcode each segment. The decoder and encoder contexts are maintained across segments, avoiding repeated initialization.

  4. FFmpeg Transcoding Tests: Runs the same segments through different FFmpeg configurations (Modes 1-3) for comparison.

  5. Results Comparison: Calculates FPS for each method and generates a comparison report.

  6. Logging: Saves detailed execution logs in JSON format for reproducibility and replay.

Running the Benchmark#

Basic Usage:

python segmented_transcode_benchmark.py

This runs the benchmark with default settings (1920x1080, 5 seconds, 10 segments, all 4 transcoding modes).

Command Line Options:

Option

Default

Description

-W, --width

1920

Video width in pixels

-H, --height

1080

Video height in pixels

-d, --duration

5400

Video duration in seconds

-fps, --fps

30

Frames per second

-s, --segments

10

Number of random segments to transcode

--segment-duration

5

Segment duration in seconds

-u, --usage

0 1 2 3

Transcoding modes to benchmark (space-separated list)

-ic, --input-codec

h264

Input codec: h264, hevc, or av1

-c, --codec

h264

Output codec: h264, hevc, or av1

-p, --preset

P1

Encoder preset (P1-P7)

-n, --numthreads

1

Number of concurrent threads

--gop-size

250

GOP size for encoding

-g, --gpuid

0

GPU device ID

-i, --input

(none)

Use existing video file instead of generating

--log

(auto)

Path to save execution log

--replay

(none)

Replay transcoding from a previous log file

Example Commands:

# Run full benchmark with default settings (all 4 modes)
python segmented_transcode_benchmark.py

# Compare only PyNvVideoCodec vs basic FFmpeg
python segmented_transcode_benchmark.py -u 0 1

# Test only PyNvVideoCodec transcoding
python segmented_transcode_benchmark.py -u 0

# Custom video parameters with 10 segments
python segmented_transcode_benchmark.py -W 1920 -H 1080 -d 30 -s 10

# Use HEVC codec with 2 B-frames
python segmented_transcode_benchmark.py -ic hevc -c hevc -bf 2

# Use an existing video file
python segmented_transcode_benchmark.py -i /path/to/video.mp4

# Replay a previous benchmark run
python segmented_transcode_benchmark.py --replay logs/run_20240615_123045.json

Output Files:

  • logs/run_{timestamp}.json - Detailed execution log

  • pynvc_out/ - Transcoded segments from PyNvVideoCodec

  • ffmpeg_out/ - Transcoded segments from FFmpeg Mode 1

  • ffmpeg_fc_out/ - Transcoded segments from FFmpeg Modes 2 and 3

  • source_videos/ - Generated source videos (reused in subsequent runs)

Expected Execution Time: Approximately 6 minutes on an L40G GPU with 3 threads.

Benchmark Environment#

Environment:

  • GPU: 1 x L40G (3 NVDECs)

  • CPU: AMD EPYC 7313P 16-Core Processor, 2 threads per core

  • OS: Ubuntu 22.04

Methodology#

  • Script to execute benchmark: segmented_transcode_benchmark.py

  • Dataset details:

    • Resolution: 1920x1080

    • Codec: H.264

    • Duration: 5400 seconds

    • Number of segments: 10

    • GOP Size: 250

    • Segment duration: 5 seconds

  • Transcoding parameters:

    • Output FPS: 30

    • Output B Frames: 0

    • Output Preset: P1

  • Benchmarks examine performance of different transcoding methods

Transcoding Methods:

  • PyNvVideoCodec transcoding: Uses PyNvVideoCodec with persistent context for segmented transcoding

  • FFmpeg without map: Uses HW accelerated FFmpeg with simple re-encoding, no mapping or container preservation

Benchmark Results#

The following results were obtained on the representative platform described in the Benchmark Environment section above. You can run the benchmark script in your own environment to obtain results specific to your hardware configuration.

Table 9 Segmented Transcoding Performance - H.264 1080p#

Method

Time (s)

Throughput (FPS)

PyNvVideoCodec transcoding

2.31

1072.34

FFmpeg without map

6.36

389.17

Bar chart comparing FFmpeg (389 FPS) with PyNvVideoCodec Segment-Based Transcoding (1072 FPS), showing 2.8x performance improvement

Figure 3 Performance Comparison: FFmpeg vs. PyNvVideoCodec Segment-Based Transcoding Bar chart comparing transcoding performance between standard FFmpeg approach and PyNvVideoCodec’s segment-based transcoding for H.264 1080p content, showing a 2.8x performance improvement#

Key Observations#

  • PyNvVideoCodec transcoding significantly outperforms FFmpeg’s standard transcoding method

  • For 1080p content, PyNvVideoCodec transcoding (1072 FPS) is approximately 2.8x faster than FFmpeg without map (389 FPS)

  • The performance advantage comes from persistent context management, avoiding repeated decoder and encoder initialization

  • This performance gain is particularly valuable for workflows that process multiple video segments, such as AI training datasets