Skip to content

torchcodec: Add version 0.16.0 - #2105

Merged
luhenry merged 7 commits into
mainfrom
torchcodec
Sep 20, 2026
Merged

luhenry merged 7 commits into
mainfrom
torchcodec

Conversation

@luhenry

@luhenry luhenry commented Sep 19, 2026

Copy link
Copy Markdown
Member

Compiles PyTorch's video decoder: a C++20 core linked against libtorch and FFmpeg, plus a separate FFmpeg-free image-decoder library. Upstream publishes no riscv64 wheel.

Mirrors upstream's linux_wheel.yaml.

Differs from upstream

  • FFmpeg built from source in the container - upstream's prebuilt non-GPL tarballs have no riscv64 build.
  • FFmpeg 6.1, so the single libtorchcodec_core6.so matches ubuntu-24.04-riscv's system FFmpeg.
  • TORCHCODEC_BUILD_AVIF=0, TORCHCODEC_BUILD_HEIC=0 - libavif comes from the same bucket, libheif is in neither Rocky 10 nor a riscv64 EPEL.
  • Warnings-as-errors off - untested compiler/arch combination.

Testing

  • torchvision not on the registry, so test_transform_ops.py is deselected.
  • Slow tests skipped (the suite's own default) rather than force-enabled as upstream's CI does.
  • The encoder tests that need a codec this FFmpeg does not have are deselected - see Second CI run below for why, and for what stays covered.

License: Wheel bundles libjpeg-turbo, libpng, zlib and libwebp (all permissive); upstream ships their licence texts from its own repair script, which this build does not use, so the build adds them.

Patches

  • 0001-pyproject-glob-the-licence-texts-of-the-bundled-image.patch - Upstream-Status: Inappropriate. Without it PEP 639's explicit license-files list silently drops the vendored-library licence texts. Not riscv64-specific, but only meaningful for a plain-auditwheel repair.
  • 0002-DecodeAvif-give-the-no-libavif-stub-the-declared-sign.patch - Upstream-Status: To upstream. DecodeAvif.cpp's !TORCHCODEC_ENABLE_AVIF stub still has the three-parameter signature from before num_threads was added, so with TORCHCODEC_BUILD_AVIF=0 the four-parameter decode_avif() that DecodeAvif.h declares and image_custom_ops.cpp registers via TORCH_BOX(&decode_avif) is defined nowhere. An ELF shared object may carry undefined symbols, so everything stays green until the first dlopen - which load_image_library() performs at import torchcodec, taking the whole package down. Not riscv64-specific; upstream never builds this path because their wheels always link the libavif they fetch from S3.

First CI run (all four jobs failed at the test step)

OSError: Could not load this library: .../torchcodec/libtorchcodec_image.so

pytest's conftest import report drops the exception chain, so the cause was not in the log. Reproduced under docker+QEMU against quay.io/pypa/manylinux_2_39_riscv64, which gives it:

OSError: .../libtorchcodec_image.so: undefined symbol:
  _ZN8facebook10torchcodec11decode_avifERKN5torch6stable6TensorElll

Patch 0002 is the fix.

Second CI run: the wheel builds and imports everywhere, and 242 tests need an encoder that does not exist here

All four interpreters got past patch 0002 - the wheel builds, repairs and imports - and then failed the same 242 tests each (242 failed, ~1515 passed, ~863 skipped; cp312/cp313/cp314/cp314t agree test-for-test). One cause, not three:

RuntimeError: validate_pixel_format, CpuDeviceInterface.cpp:43, Specified pixel format yuv444p is not supported by the mpeg4 encoder.
RuntimeError: initialize_video_stream, Encoder.cpp:427, Video codec av1 not found. To see available codecs, run: ffmpeg -encoders
RuntimeError: initialize_video_stream, Encoder.cpp:500, avcodec_open2 failed: Invalid argument
RuntimeError: initialize_audio_stream, Encoder.cpp:530, Codec not found
subprocess.CalledProcessError: Command '['ffmpeg', ..., 'ffmpeg_output.mp3']' returned non-zero exit status 8.

Every modern video encoder, and the MP3 one, lives in an external FFmpeg library, and this build takes none of them: ./configure --disable-static --enable-shared --enable-pic --disable-doc reports External libraries: iconv libxcb* zlib and defines no CONFIG_*_ENCODER for H.264, HEVC, VP9, AV1 or MP3 (verified by running that exact configure on FFmpeg 6.1.6 - the codec set is architecture-independent). FFmpeg then falls back to the container format's default codec, which is why the failures accuse mpeg4 of not taking yuv444p and avcodec_open2 of rejecting crf/preset/profile, rather than saying "no H.264 encoder". test_get_frame_at_av1 is the decode-side face of the same thing: FFmpeg 6.1's native AV1 decoder only runs through a hardware accelerator (libavcodec/av1dec.c: "Your platform doesn't suppport hardware accelerated AV1 decoding"), software AV1 decoding means libdav1d or libaom.

Upstream never sees this because its test job does conda install ffmpeg -c conda-forge (packaging/install_ffmpeg.sh), whose default build is the GPL one - hence test ids like test_codec_spec_vs_impl_equivalence[h264-libx264-to_file]. Two of the missing libraries, libx264 and libx265, can only be enabled with --enable-gpl, which is exactly what a wheel that links FFmpeg must not be built against, so this surface is not fixable here: it is deselected instead.

What is dropped, and what deliberately is not:

  • The tests that mux a real video, shell out to the ffmpeg CLI, or ask for MP3.
  • smoke_test.py entirely - every fixture it decodes is built by encoding H.264 yuv444p first, which is why its TestVideoDecoder cases are in the failure list at all.
  • test_logging.py entirely - it asserts an empty subprocess stderr, which torch's libcpuinfo pollutes on this runner by reading core_id -1 from /sys (Error in cpuinfo: failed to parse file /sys/devices/system/cpu/cpu0/topology/core_id). Unrelated to FFmpeg, and not something the wheel can fix.
  • Kept: the MP3 clauses are guarded (not ((test_audio_against_cli or ...) and mp3)), so the WAV and FLAC parametrisations of those same tests still run - the audio encoder, the image encoder (TestImageEncoders, PNG round-trip and the PIL comparison), the multi-stream encoder's error paths and every decoder module stay covered.

The shipped wheel bundles no FFmpeg, so none of this reaches users: on ubuntu-24.04-riscv the system FFmpeg supplies libx264, libvpx, libaom and dav1d, and encoding works there. The limitation is the container's, not the wheel's.

Local validation

  • First fix (docker + binfmt QEMU, quay.io/pypa/manylinux_2_39_riscv64, cp312): the unpatched build reproduces the CI traceback exactly; with 0002 the repaired wheel imports (IMPORT_OK 0.16.0 6), ldd -r reports no unresolved symbol in any of the five shipped libraries (only the Py* ones the interpreter provides), and the workflow's own wheel-contents check passes - libtorchcodec_{core6,custom_ops6,heic,image,pybind_ops}.so plus grafted libjpeg/libpng16/libwebp/libwebpdemux/libsharpyuv, no FFmpeg and no libtorch, and the three third-party licence texts in .dist-info/licenses/.
  • Second fix: FFmpeg 6.1.6 configured with the workflow's exact line in a manylinux container to confirm the codec set above. The test selection was then checked without spending a runner cycle: the 242 failing node ids were regenerated as a throwaway pytest tree (matching ids character for character, plus known-passing ids), and the CIBW_TEST_COMMAND was read back out of the YAML and executed through sh -c the way cibuildwheel invokes it. It deselects exactly the 232 in-scope failures (the other 10 are the two ignored modules) and no test that passed.

torchcodec is PyTorch's video decoder: a scikit-build-core/CMake project that
compiles a C++20 core against libtorch and FFmpeg, plus a separate,
FFmpeg-free image-decoder library.

Upstream links its wheels against prebuilt non-GPL FFmpeg tarballs it hosts on
S3, which carry no riscv64 build, so the workflow takes the project's other
supported path -- pkg-config against an installed FFmpeg -- and builds that
FFmpeg from source in the container, LGPL and never shipped. FFmpeg 6.1 because
the major version is baked into the libtorchcodec_core<N>.so name the Python
loader dlopen()s, and 6.1 is what ubuntu-24.04-riscv ships.

AVIF and HEIC image decoding are off: libavif comes from the same S3 bucket and
libheif is packaged in neither Rocky 10 nor a riscv64 EPEL. JPEG, PNG, WebP and
the vendored giflib are built as usual.

Also records two reusable gotchas found along the way (383, 384).
RPM installs %license files regardless of tsflags=nodocs (only %doc is
dropped), and all four packages whose licences the wheel needs mark them
%license -- confirmed against the CentOS Stream 10 spec files, which are also
where `%files devel` confirms libwebp-devel ships the CMake package config
find_package(WebP) needs. The reinstall stays as belt-and-braces but must not
fail the build when a package is no longer available from a repo.
@luhenry luhenry changed the title torchcodec: build 0.16.0 wheels for riscv64 torchcodec: Add version 0.16.0 Sep 19, 2026
Restores skills/python-project-porting/references/{gotchas-index.md,
gotchas/local-validation-and-rehearsal.md, gotchas/native-deps-and-linking.md}
to main's current content. The two gotchas this PR had added (383/384,
torch+CUDA rehearsal and versioned-dlopen ABI contract) also collided with
gotcha numbers other agents had already taken on main in the meantime -
they'll be re-added on main directly with fresh numbers instead.
luhenry added a commit that referenced this pull request Sep 19, 2026
These were originally added inside PR #2105 (torchcodec), which is not
allowed to touch skills/ - removed from that branch and landed here
instead, renumbered from their original 383/384 (already taken by other
concurrent work by the time this was caught).
…is branch no longer touches

# Conflicts:
#	skills/python-project-porting/references/gotchas-index.md
#	skills/python-project-porting/references/gotchas/local-validation-and-rehearsal.md
@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-20 08:08 UTC

…y loads

All four riscv64 jobs of the first CI run built, repaired and installed a
wheel and then died at the test step with

  OSError: Could not load this library:
    .../site-packages/torchcodec/libtorchcodec_image.so

pytest's conftest import report drops the exception chain, so the cause
never reached the log. Reproducing the run under docker+QEMU against
quay.io/pypa/manylinux_2_39_riscv64 gives it:

  OSError: .../libtorchcodec_image.so: undefined symbol:
    _ZN8facebook10torchcodec11decode_avifERKN5torch6stable6TensorElll

that is facebook::torchcodec::decode_avif(torch::stable::Tensor const&,
long, long, long). DecodeAvif.h declares decode_avif() with four
parameters, the real implementation defines four, and
image_custom_ops.cpp registers it with TORCH_BOX(&decode_avif) - but the
!TORCHCODEC_ENABLE_AVIF stub this build selects still has the
three-parameter signature from before num_threads was added, so it
defines a different overload and the four-parameter one exists nowhere.
An ELF shared object may carry undefined symbols, so the link, the wheel
build and auditwheel all stay green and the first dlopen is where it
fails - and since load_image_library() runs at `import torchcodec`, that
takes the whole package down, not just decode_avif.

Upstream never builds this path: their wheels always link the libavif
they fetch from S3, and no CI job sets TORCHCODEC_BUILD_AVIF=0. We have
to, because that bucket has no riscv64 build and libavif is packaged in
neither Rocky 10 nor a riscv64 EPEL.

Validated under docker+QEMU on quay.io/pypa/manylinux_2_39_riscv64,
cp312: the unpatched wheel reproduces the CI traceback exactly, the
patched one imports (`IMPORT_OK 0.16.0 6`), `ldd -r` reports no
unresolved symbol in any of the five shipped libraries, and the
workflow's own wheel-contents check passes on the repaired wheel.
luhenry added a commit that referenced this pull request Sep 19, 2026
… flight

All four riscv64 jobs of PR #2105's first run failed at the test step on
an unloadable libtorchcodec_image.so. Reproduced the job under
docker+QEMU on the real manylinux riscv64 image: torchcodec's
no-libavif decode_avif() stub has a stale three-parameter signature, so
with TORCHCODEC_BUILD_AVIF=0 the four-parameter overload the op
registration takes the address of is defined nowhere and the library
carries an undefined symbol that only surfaces at dlopen. Patch 0002
fixes it; the patched wheel imports and ldd -r is clean.
The second CI run built and imported the wheel on all four interpreters and then
failed the same 242 tests on each of them. Every one of those tests needs an
encoder FFmpeg does not have: upstream tests against conda-forge's GPL FFmpeg,
while the build here configures FFmpeg with no external libraries, so it has none
of the H.264, HEVC, VP9, AV1 or MP3 encoders (every one of them lives in an
external library, and libx264/libx265 need --enable-gpl) and its AV1 decoder only
works through a hardware accelerator. The symptoms all follow from that: FFmpeg
falls back to the container format's default codec, so the failures read "yuv444p
is not supported by the mpeg4 encoder", "avcodec_open2 failed: Invalid argument",
"Video codec av1 not found" or "Codec not found", and the ffmpeg CLI the encoder
tests compare themselves against cannot write an MP3 either.

Drop the tests that mux a real video, shell out to that CLI or ask for MP3, and
smoke_test.py with them - it encodes H.264 yuv444p to build every fixture it then
decodes. test_logging.py goes too: it asserts an empty subprocess stderr, which
torch's libcpuinfo pollutes on this runner by reading core_id -1 from /sys.

The selection is deliberately narrow - the MP3 clauses keep the WAV and FLAC
parametrisations of the same tests, so the audio encoder, the image encoder and
every decoder stay covered. Checked against the node ids of the failed run: the
expression deselects exactly those 232 tests and nothing that passed.
@luhenry
luhenry merged commit bf73979 into main Sep 20, 2026
12 checks passed
@luhenry
luhenry deleted the torchcodec branch September 20, 2026 07:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant