-
Notifications
You must be signed in to change notification settings - Fork 74
Fix Windows build and add Windows CPU tests #806
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 63 commits
d81c3e3
78ea520
a0dd832
1b5f926
218ca2d
4cce232
ab53ca9
8450297
3b79ff5
e4f00a4
ac8116d
9573fc2
963590e
2f2c91d
d896211
01f9dc0
60df908
09d077e
61ef07b
1ae09aa
6921d4c
dab2d8e
ff91aca
bc919cc
d988393
9da6539
99d0308
7247adc
11a7c55
2bdb643
7b12d7d
e7969d3
cbff515
f7c8bd5
02ad2d7
8764c60
110f5b0
f0edf32
aacbfc6
fc17647
5da6109
99cedf1
fd0b7d5
e08b5e7
f073d1f
2f51f25
ae8a2c6
3b06000
06d0627
5903c07
c7c6077
7822d73
8a30f92
b7b0ea9
f25a0a7
89e213d
281cc38
f2c3414
12df6c7
9048f6c
c02ffc5
4e4f739
e40dfc2
ff6d5d1
f83e9da
23c20de
7657857
b531d4b
d89f81a
5171e8c
3892751
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,9 @@ jobs: | |
| with-rocm: disable | ||
| with-cuda: disable | ||
| build-python-only: "disable" | ||
| # Explicitly avoid 3.13 because 3.13t builds don't work. | ||
| # TODO remove eventually. | ||
| python-versions: '["3.9", "3.10", "3.11", "3.12"]' | ||
|
|
||
| build: | ||
| needs: generate-matrix | ||
|
|
@@ -60,3 +63,74 @@ jobs: | |
| # The BUILD_AGAINST_ALL_FFMPEG_FROM_S3 var, needed to build the wheel, is | ||
| # set in vc_env_helper.bat Couldn't find a way to set it from here. | ||
| build-command: "python -m build --wheel -vvv --no-isolation" | ||
|
|
||
| install-and-test: | ||
| runs-on: windows-latest | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| python-version: ['3.9', '3.10', '3.11', '3.12'] | ||
|
||
| # TODO: FFmpeg 5 on Windows segfaults in avcodec_open2() when passing | ||
| # bad parameters. | ||
| # See https://github.com/pytorch/torchcodec/pull/806 | ||
| ffmpeg-version-for-tests: ['4.4.2', '6.1.1', '7.0.1'] | ||
| needs: build | ||
| steps: | ||
| - uses: actions/download-artifact@v4 | ||
| with: | ||
| name: pytorch_torchcodec__${{ matrix.python-version }}_cpu_x64 | ||
| path: pytorch/torchcodec/dist/ | ||
| - name: Setup conda env | ||
| uses: conda-incubator/setup-miniconda@v2 | ||
| with: | ||
| auto-update-conda: true | ||
| miniconda-version: "latest" | ||
| activate-environment: test | ||
| python-version: ${{ matrix.python-version }} | ||
| - name: Update pip | ||
| run: python -m pip install --upgrade pip | ||
| - name: Install PyTorch | ||
| run: | | ||
| python -m pip install --pre torch --index-url https://download.pytorch.org/whl/nightly/cpu | ||
| - name: Install torchcodec from the wheel | ||
| run: | | ||
| wheel_path=`find pytorch/torchcodec/dist -type f -name "*.whl"` | ||
| echo Installing $wheel_path | ||
| python -m pip install $wheel_path -vvv | ||
| - name: Check out repo | ||
| uses: actions/checkout@v3 | ||
| - name: Install ffmpeg, post build | ||
| run: | | ||
| # Ideally we would have checked for that before installing the wheel, | ||
| # but we need to checkout the repo to access this file, and we don't | ||
| # want to checkout the repo before installing the wheel to avoid any | ||
| # side-effect. It's OK. | ||
| source packaging/helpers.sh | ||
| assert_ffmpeg_not_installed | ||
| conda install "ffmpeg=${{ matrix.ffmpeg-version-for-tests }}" -c conda-forge | ||
| ffmpeg -version | ||
| - name: Test torchcodec import after FFmpeg installation | ||
| run: | | ||
| echo "Testing torchcodec import after FFmpeg is installed and PATH is updated..." | ||
| python -c "import torchcodec; print('TorchCodec import successful!')" | ||
| - name: Install test dependencies | ||
| run: | | ||
| # Ideally we would find a way to get those dependencies from pyproject.toml | ||
| python -m pip install numpy pytest pillow | ||
| - name: Delete the src/ folder just for fun | ||
| run: | | ||
| # The only reason we checked-out the repo is to get access to the | ||
| # tests. We don't care about the rest. Out of precaution, we delete | ||
| # the src/ folder to be extra sure that we're running the code from | ||
| # the installed wheel rather than from the source. | ||
| # This is just to be extra cautious and very overkill because a) | ||
| # there's no way the `torchcodec` package from src/ can be found from | ||
| # the PythonPath: the main point of `src/` is precisely to protect | ||
| # against that and b) if we ever were to execute code from | ||
| # `src/torchcodec`, it would fail loudly because the built .so files | ||
| # aren't present there. | ||
| rm -r src/ | ||
| ls | ||
| - name: Run Python tests | ||
| run: | | ||
| pytest test -vvv | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -135,9 +135,14 @@ def _build_all_extensions_with_cmake(self): | |
| ["cmake", str(_ROOT_DIR)] + cmake_args, cwd=self.build_temp | ||
| ) | ||
| print("Calling cmake --build", flush=True) | ||
| subprocess.check_call(["cmake", "--build", "."], cwd=self.build_temp) | ||
| subprocess.check_call( | ||
| ["cmake", "--build", ".", "--config", cmake_build_type], cwd=self.build_temp | ||
| ) | ||
| print("Calling cmake --install", flush=True) | ||
| subprocess.check_call(["cmake", "--install", "."], cwd=self.build_temp) | ||
| subprocess.check_call( | ||
| ["cmake", "--install", ".", "--config", cmake_build_type], | ||
| cwd=self.build_temp, | ||
| ) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Above is the main fix that was needed for the extensions to load properly, something I had been blocked on for a few weeks. The fix is from @traversaro : IIUC, on windows and with our current setup, cmake is generating multiple build files. And without this fix, the different build files would be inheriting different build configuration. Typically we'd be building some parts with the @traversaro thank you so much again for unblocking us! |
||
|
|
||
| def copy_extensions_to_source(self): | ||
| """Copy built extensions from temporary folder back into source tree. | ||
|
|
@@ -156,7 +161,7 @@ def copy_extensions_to_source(self): | |
| # https://stackoverflow.com/a/2339910 | ||
| extensions = ["dylib", "so"] | ||
| elif sys.platform in ("win32", "cygwin"): | ||
| extensions = ["dll"] | ||
| extensions = ["dll", "pyd"] | ||
| else: | ||
| raise NotImplementedError(f"Platform {sys.platform} is not supported") | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,20 +53,6 @@ function(make_torchcodec_sublibrary | |
| # Avoid adding the "lib" prefix which we already add explicitly. | ||
| set_target_properties(${library_name} PROPERTIES PREFIX "") | ||
|
|
||
| if(WIN32) | ||
| # On Windows, the built artifacts are put in Release/Debug | ||
| # subdirectories by default. We want to avoid that, otherwise our | ||
| # install() step would not know where to find those. | ||
| set_target_properties(${library_name} PROPERTIES | ||
| RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_CURRENT_BINARY_DIR} | ||
| RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_BINARY_DIR} | ||
| LIBRARY_OUTPUT_DIRECTORY_DEBUG ${CMAKE_CURRENT_BINARY_DIR} | ||
| LIBRARY_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_BINARY_DIR} | ||
| ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${CMAKE_CURRENT_BINARY_DIR} | ||
| ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_BINARY_DIR} | ||
| ) | ||
| endif() | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @traversaro correctly pointed out that this can now be removed, as a consequence of the other |
||
| target_link_libraries( | ||
| ${library_name} | ||
| PUBLIC | ||
|
|
@@ -84,16 +70,17 @@ function(make_torchcodec_libraries | |
| # | ||
| # 1. libtorchcodec_coreN.{ext}: Base library which contains the | ||
| # implementation of VideoDecoder and everything VideoDecoder needs. On | ||
| # Linux, {ext} is so. On Mac, it is dylib. | ||
| # Linux, {ext} is so. On Mac, it is dylib. On Windows it's dll. | ||
| # | ||
| # 2. libtorchcodec_custom_opsN.{ext}: Implementation of the PyTorch custom | ||
| # ops. Depends on libtorchcodec_coreN.{ext}. On Linux, {ext} is so. | ||
| # On Mac, it is dylib. | ||
| # On Mac, it is dylib. On Windows it's dll. | ||
| # | ||
| # 3. libtorchcodec_pybind_opsN.{ext}: Implementation of the pybind11 ops. We | ||
| # keep these separate from the PyTorch custom ops because we have to | ||
| # load these libraries separately on the Python side. Depends on | ||
| # libtorchcodec_coreN.{ext}. On BOTH Linux and Mac {ext} is so. | ||
| # libtorchcodec_coreN.{ext}. On BOTH Linux and Mac {ext} is so. On | ||
| # Windows, it's pyd. | ||
|
|
||
| # 1. Create libtorchcodec_coreN.{ext}. | ||
| set(core_library_name "libtorchcodec_core${ffmpeg_major_version}") | ||
|
|
@@ -174,6 +161,14 @@ function(make_torchcodec_libraries | |
| "${pybind_ops_sources}" | ||
| "${pybind_ops_dependencies}" | ||
| ) | ||
|
|
||
| if(WIN32) | ||
| # On Windows, we need to set the suffix to .pyd so that Python can | ||
| # import the shared library as a module. Just setting the MODULE type | ||
| # isn't enough. | ||
| set_target_properties(${pybind_ops_library_name} PROPERTIES SUFFIX ".pyd") | ||
| endif() | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one is actually important, tried removing it in #826 and loading fails. |
||
| # pybind11 limits the visibility of symbols in the shared library to prevent | ||
| # stray initialization of py::objects. The rest of the object code must | ||
| # match. See: | ||
|
|
@@ -223,7 +218,7 @@ function(make_torchcodec_libraries | |
| install( | ||
| TARGETS ${all_libraries} | ||
| LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX} | ||
| RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX} # For Windows DLLs | ||
| RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX} # For Windows | ||
| ) | ||
|
|
||
| endfunction() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -104,10 +104,14 @@ AudioEncoder::~AudioEncoder() { | |
|
|
||
| void AudioEncoder::close_avio() { | ||
| if (avFormatContext_ && avFormatContext_->pb) { | ||
| avio_flush(avFormatContext_->pb); | ||
| if (avFormatContext_->pb->error == 0) { | ||
| avio_flush(avFormatContext_->pb); | ||
| } | ||
|
|
||
| if (!avioContextHolder_) { | ||
| avio_close(avFormatContext_->pb); | ||
| if (avFormatContext_->pb->error == 0) { | ||
| avio_close(avFormatContext_->pb); | ||
| } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Above is a drive-by as I was trying to fix the problem with FFmpeg 5. I'm not sure it fixed anything, but it is probably still a good change to have? I can extract it out in another PR if it's preferred.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this code imply that if the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on my digging, it's also that it's risky to call flush when there's an error state. Seems reasonable to keep it in.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, it because when error isn't 0 then flushing or calling |
||
| // avoids closing again in destructor, which would segfault. | ||
| avFormatContext_->pb = nullptr; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
| assert_tensor_close_on_at_least, | ||
| get_ffmpeg_major_version, | ||
| in_fbcode, | ||
| IS_WINDOWS, | ||
| NASA_AUDIO_MP3, | ||
| SINE_MONO_S32, | ||
| TestContainerFile, | ||
|
|
@@ -151,15 +152,29 @@ def test_bad_input_parametrized(self, method, tmp_path): | |
| raise ValueError(f"Unknown method: {method}") | ||
|
|
||
| decoder = AudioEncoder(self.decode(NASA_AUDIO_MP3).data, sample_rate=10) | ||
| with pytest.raises(RuntimeError, match="invalid sample rate=10"): | ||
| avcodec_open2_failed_msg = "avcodec_open2 failed: Invalid argument" | ||
| with pytest.raises( | ||
| RuntimeError, | ||
| match=avcodec_open2_failed_msg if IS_WINDOWS else "invalid sample rate=10", | ||
| ): | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. above and in other tests below: on Windows, we're hitting this early return: So we're unable to validate some parameters early, and we just fail in the call to |
||
| getattr(decoder, method)(**valid_params) | ||
|
|
||
| decoder = AudioEncoder( | ||
| self.decode(NASA_AUDIO_MP3).data, sample_rate=NASA_AUDIO_MP3.sample_rate | ||
| ) | ||
| with pytest.raises(RuntimeError, match="invalid sample rate=10"): | ||
| with pytest.raises( | ||
| RuntimeError, | ||
| match=avcodec_open2_failed_msg if IS_WINDOWS else "invalid sample rate=10", | ||
| ): | ||
| getattr(decoder, method)(sample_rate=10, **valid_params) | ||
| with pytest.raises(RuntimeError, match="invalid sample rate=99999999"): | ||
| with pytest.raises( | ||
| RuntimeError, | ||
| match=( | ||
| avcodec_open2_failed_msg | ||
| if IS_WINDOWS | ||
| else "invalid sample rate=99999999" | ||
| ), | ||
| ): | ||
| getattr(decoder, method)(sample_rate=99999999, **valid_params) | ||
| with pytest.raises(RuntimeError, match="bit_rate=-1 must be >= 0"): | ||
| getattr(decoder, method)(**valid_params, bit_rate=-1) | ||
|
|
@@ -175,12 +190,14 @@ def test_bad_input_parametrized(self, method, tmp_path): | |
| self.decode(NASA_AUDIO_MP3).data, sample_rate=NASA_AUDIO_MP3.sample_rate | ||
| ) | ||
| for num_channels in (0, 3): | ||
| with pytest.raises( | ||
| RuntimeError, | ||
| match=re.escape( | ||
| match = ( | ||
| avcodec_open2_failed_msg | ||
| if IS_WINDOWS | ||
| else re.escape( | ||
| f"Desired number of channels ({num_channels}) is not supported" | ||
| ), | ||
| ): | ||
| ) | ||
| ) | ||
| with pytest.raises(RuntimeError, match=match): | ||
| getattr(decoder, method)(**valid_params, num_channels=num_channels) | ||
|
|
||
| @pytest.mark.parametrize("method", ("to_file", "to_tensor", "to_file_like")) | ||
|
|
@@ -240,6 +257,9 @@ def test_against_cli( | |
|
|
||
| if get_ffmpeg_major_version() == 4 and format == "wav": | ||
| pytest.skip("Swresample with FFmpeg 4 doesn't work on wav files") | ||
| if IS_WINDOWS and get_ffmpeg_major_version() <= 5 and format == "mp3": | ||
| # TODO: https://github.com/pytorch/torchcodec/issues/837 | ||
| pytest.skip("Encoding mp3 on Windows is weirdly buggy") | ||
|
|
||
| encoded_by_ffmpeg = tmp_path / f"ffmpeg_output.{format}" | ||
| subprocess.run( | ||
|
|
@@ -295,8 +315,15 @@ def test_against_cli( | |
| rtol, atol = 0, 1e-3 | ||
| else: | ||
| rtol, atol = None, None | ||
|
|
||
| if IS_WINDOWS and format == "mp3": | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this condition be hit? It might be duplicate with the check and
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The check above only skips on FFmpeg <= 5, but FFmpeg 6 and 7 will still reach this line :) |
||
| # We're getting a "Could not open input file" on Windows mp3 files when decoding. | ||
| # TODO: https://github.com/pytorch/torchcodec/issues/837 | ||
| return | ||
|
|
||
| samples_by_us = self.decode(encoded_by_us) | ||
| samples_by_ffmpeg = self.decode(encoded_by_ffmpeg) | ||
|
|
||
| assert_close( | ||
| samples_by_us.data, | ||
| samples_by_ffmpeg.data, | ||
|
|
@@ -320,6 +347,9 @@ def test_against_to_file( | |
| ): | ||
| if get_ffmpeg_major_version() == 4 and format == "wav": | ||
| pytest.skip("Swresample with FFmpeg 4 doesn't work on wav files") | ||
| if IS_WINDOWS and get_ffmpeg_major_version() <= 5 and format == "mp3": | ||
| # TODO: https://github.com/pytorch/torchcodec/issues/837 | ||
| pytest.skip("Encoding mp3 on Windows is weirdly buggy") | ||
|
|
||
| encoder = AudioEncoder(self.decode(asset).data, sample_rate=asset.sample_rate) | ||
|
|
||
|
|
@@ -340,9 +370,12 @@ def test_against_to_file( | |
| else: | ||
| raise ValueError(f"Unknown method: {method}") | ||
|
|
||
| torch.testing.assert_close( | ||
| self.decode(encoded_file).data, self.decode(encoded_output).data | ||
| ) | ||
| if not (IS_WINDOWS and format == "mp3"): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same question here, is this a duplicate check with line 319? On a similar note, it might be better to explicitly call
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not a duplicate because this will be reached by FFmpeg 6 and 7. You're right that we should generally prefer using In this specific case I think it's best not to call |
||
| # We're getting a "Could not open input file" on Windows mp3 files when decoding. | ||
| # TODO: https://github.com/pytorch/torchcodec/issues/837 | ||
| torch.testing.assert_close( | ||
| self.decode(encoded_file).data, self.decode(encoded_output).data | ||
| ) | ||
|
|
||
| def test_encode_to_tensor_long_output(self): | ||
| # Check that we support re-allocating the output tensor when the encoded | ||
|
|
@@ -414,7 +447,7 @@ def test_num_channels( | |
|
|
||
| sample_rate = 16_000 | ||
| source_samples = torch.rand(num_channels_input, 1_000) | ||
| format = "mp3" | ||
| format = "flac" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this change enable the test to pass on Windows? If so, we could parameterize the format and add an
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct, I changed from mp3 to flac due to the issues we had on Windows. I agree that we could parameterize, but I'm not sure it's necessary for the purpose of this test, as it wouldn't make it more robust. Mostly this test is just about checking the number of output channels is respected, which is agnostic to the format. |
||
|
|
||
| encoder = AudioEncoder(source_samples, sample_rate=sample_rate) | ||
| params = dict(num_channels=num_channels_output) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will remove this before merging. This is just to show that the jobs are green across versions.
Note that we may still try to address the 3.13t issue, but that's for a follow-up.