From e158dce3318970cc92b9a50840fd8ebf86ff23b5 Mon Sep 17 00:00:00 2001 From: alexeykozhevin Date: Fri, 20 Feb 2026 11:30:50 +0000 Subject: [PATCH 1/4] Update release workflows --- .github/workflows/release-check.yml | 38 --- .../workflows/release-on-branch-create.yml | 77 ++++++ .github/workflows/release-on-merge.yml | 251 ++++++++++++++++++ .github/workflows/release.yml | 59 ---- 4 files changed, 328 insertions(+), 97 deletions(-) delete mode 100644 .github/workflows/release-check.yml create mode 100644 .github/workflows/release-on-branch-create.yml create mode 100644 .github/workflows/release-on-merge.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml deleted file mode 100644 index 1b2f986d6..000000000 --- a/.github/workflows/release-check.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: release-check - -on: - release: - types: [created, edited] - - -jobs: - - pypi: - - runs-on: ubuntu-latest - environment: release - permissions: - id-token: write - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python 3.9 - uses: actions/setup-python@v4 - with: - python-version: 3.9 - - - name: Install dependencies - run: | - pip install --user -U pip uv - uv sync - - - name: Build - run: | - uv pip install build - uv run python -m build - - - name: Publish - uses: pypa/gh-action-pypi-publish@release/v1 - with: - repository-url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/release-on-branch-create.yml b/.github/workflows/release-on-branch-create.yml new file mode 100644 index 000000000..26acf79cc --- /dev/null +++ b/.github/workflows/release-on-branch-create.yml @@ -0,0 +1,77 @@ +name: Handle Release Branch Creation + +on: + create: + branches: + - 'r*' + +permissions: + contents: write + pull-requests: write + +jobs: + validate: + runs-on: ubuntu-latest + outputs: + is_valid: ${{ steps.check.outputs.is_valid }} + steps: + - name: Validate branch name + id: check + run: | + if [[ "${{ github.ref_name }}" =~ ^r[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "is_valid=true" >> $GITHUB_OUTPUT + else + echo "is_valid=false" >> $GITHUB_OUTPUT + fi + + prepare: + needs: validate + if: needs.validate.outputs.is_valid == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + fetch-depth: 0 + + - name: Get version from branch + id: get_version + run: | + VERSION=$(echo "${{ github.ref_name }}" | sed -e 's/^r//') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Update version in pyproject.toml + run: | + sed -i "s/^version = \".*\"/version = \"${{ steps.get_version.outputs.version }}\"/" pyproject.toml + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + python-version: "3.11" + + - name: Update lock file + run: uv lock --python 3.11 + + - name: Commit and push changes + id: autocommit + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "Update version to ${{ steps.get_version.outputs.version }}" + branch: ${{ github.ref_name }} + file_pattern: "pyproject.toml uv.lock" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create Pull Request + if: steps.autocommit.outputs.changes_detected == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr create \ + --base master \ + --head ${{ github.ref_name }} \ + --title "Release ${{ github.ref_name }}" \ + --body "This PR contains the changes for release ${{ github.ref_name }}." \ + --draft diff --git a/.github/workflows/release-on-merge.yml b/.github/workflows/release-on-merge.yml new file mode 100644 index 000000000..2cacf86c9 --- /dev/null +++ b/.github/workflows/release-on-merge.yml @@ -0,0 +1,251 @@ +name: Release Cycle + +on: + pull_request: + types: [closed] + branches: + - master + +permissions: + contents: write + pull-requests: write + +jobs: + + # --------------------------------------------------------------------------- + # Create GitHub release and tag; optionally create next minor branch + # --------------------------------------------------------------------------- + release: + if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'r') + runs-on: ubuntu-latest + outputs: + new_branch: ${{ steps.create_next_branch.outputs.new_branch }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get version from branch + id: get_version + run: | + VERSION=$(echo "${{ github.event.pull_request.head.ref }}" | sed -e 's/^r//') + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag_name=v$VERSION" >> $GITHUB_OUTPUT + + - name: Create Release and Tag + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create ${{ steps.get_version.outputs.tag_name }} \ + --generate-notes \ + --title "${{ steps.get_version.outputs.tag_name }}" + + - name: Create next release branch if major/minor + id: create_next_branch + if: endsWith(steps.get_version.outputs.version, '.0') + run: | + VERSION="${{ steps.get_version.outputs.version }}" + MAJOR=$(echo "$VERSION" | cut -d. -f1) + MINOR=$(echo "$VERSION" | cut -d. -f2) + NEW_MINOR=$((MINOR + 1)) + NEW_BRANCH="r$MAJOR.$NEW_MINOR.0" + git checkout -b "$NEW_BRANCH" "${{ github.sha }}" + git push -u origin "$NEW_BRANCH" + echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT + + # --------------------------------------------------------------------------- + # Publish to Test PyPI + # --------------------------------------------------------------------------- + pypi-test: + needs: release + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + python-version: "3.11" + + - name: Build + run: uv build + + - name: Publish to Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + + # --------------------------------------------------------------------------- + # Publish to PyPI + # --------------------------------------------------------------------------- + pypi: + needs: pypi-test + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + python-version: "3.11" + + - name: Build + run: uv build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + # --------------------------------------------------------------------------- + # Prepare the next release branch (version bump + lock + PR) + # --------------------------------------------------------------------------- + prepare-next: + needs: release + if: needs.release.outputs.new_branch != '' + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ needs.release.outputs.new_branch }} + fetch-depth: 0 + + - name: Get version from branch + id: get_version + run: | + VERSION=$(echo "${{ needs.release.outputs.new_branch }}" | sed -e 's/^r//') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Update version in pyproject.toml + run: | + sed -i "s/^version = \".*\"/version = \"${{ steps.get_version.outputs.version }}\"/" pyproject.toml + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + python-version: "3.11" + + - name: Update lock file + run: uv lock --python 3.11 + + - name: Commit and push changes + id: autocommit + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "Update version to ${{ steps.get_version.outputs.version }}" + branch: ${{ needs.release.outputs.new_branch }} + file_pattern: "pyproject.toml uv.lock" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create Pull Request + if: steps.autocommit.outputs.changes_detected == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr create \ + --base master \ + --head ${{ needs.release.outputs.new_branch }} \ + --title "Release ${{ needs.release.outputs.new_branch }}" \ + --body "This PR contains the changes for release ${{ needs.release.outputs.new_branch }}." \ + --draft + + # --------------------------------------------------------------------------- + # Propagate hotfix to the next open release branch (patch releases only) + # --------------------------------------------------------------------------- + propagate-hotfix: + if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'r') + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.repository.default_branch }} + fetch-depth: 0 + + - name: Check if hotfix propagation is needed + id: check + run: | + MERGED_BRANCH="${{ github.event.pull_request.head.ref }}" + if [[ ! $MERGED_BRANCH =~ ^r([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "Branch $MERGED_BRANCH is not a valid release branch. Skipping." + echo "should_propagate=false" >> $GITHUB_OUTPUT + exit 0 + fi + + MAJOR=${BASH_REMATCH[1]} + MINOR=${BASH_REMATCH[2]} + PATCH=${BASH_REMATCH[3]} + + if [[ $PATCH -eq 0 ]]; then + echo "Not a patch release (Z=0). No forward-merge needed." + echo "should_propagate=false" >> $GITHUB_OUTPUT + exit 0 + fi + + NEXT_MINOR=$((MINOR + 1)) + TARGET_VERSION="$MAJOR.$NEXT_MINOR.0" + TARGET_BRANCH="r$TARGET_VERSION" + + if ! git ls-remote --exit-code --heads origin "$TARGET_BRANCH"; then + echo "Target branch $TARGET_BRANCH does not exist. No forward-merge needed." + echo "should_propagate=false" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "should_propagate=true" >> $GITHUB_OUTPUT + echo "target_branch=$TARGET_BRANCH" >> $GITHUB_OUTPUT + echo "target_version=$TARGET_VERSION" >> $GITHUB_OUTPUT + echo "merged_version=$MAJOR.$MINOR.$PATCH" >> $GITHUB_OUTPUT + + - name: Install uv + if: steps.check.outputs.should_propagate == 'true' + uses: astral-sh/setup-uv@v5 + with: + enable-cache: false + python-version: "3.11" + + - name: Propagate hotfix + if: steps.check.outputs.should_propagate == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TARGET_BRANCH="${{ steps.check.outputs.target_branch }}" + TARGET_VERSION="${{ steps.check.outputs.target_version }}" + MERGED_VERSION="${{ steps.check.outputs.merged_version }}" + TEMP_BRANCH="hotfix-propagate-v$MERGED_VERSION-to-$TARGET_BRANCH" + DEFAULT_BRANCH="${{ github.event.repository.default_branch }}" + + echo "Propagating hotfix v$MERGED_VERSION to $TARGET_BRANCH" + git checkout -b "$TEMP_BRANCH" "origin/$DEFAULT_BRANCH" + + # Update version in pyproject.toml + sed -i "s/^version = \".*\"/version = \"$TARGET_VERSION\"/" pyproject.toml + + # Update lock file + uv lock --python 3.11 + + # Commit and push + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add pyproject.toml uv.lock + git commit -m "Align version to $TARGET_VERSION for hotfix propagation" + git push -u origin "$TEMP_BRANCH" + + # Create PR + gh pr create \ + --base "$TARGET_BRANCH" \ + --head "$TEMP_BRANCH" \ + --title "Merge hotfix changes from $DEFAULT_BRANCH into $TARGET_BRANCH" \ + --body "This PR propagates hotfix changes from v$MERGED_VERSION into the next release branch ($TARGET_BRANCH)." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index c471a3e9a..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: release - -on: - release: - types: [released] - - -jobs: - - codecov: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python 3.9 - uses: actions/setup-python@v4 - with: - python-version: 3.9 - - - name: Generate coverage report - run: | - pip install uv - uv sync --all-extras - uv pip install -U pytest-cov - uv run pytest -m "not slow" --cov=./ --cov-report=xml - - - name: Upload coverage to Codecov - run: | - uv pip install -U codecov - uv run codecov -t ${{ secrets.CODECOV_TOKEN }} - - - pypi: - runs-on: ubuntu-latest - environment: release - permissions: - id-token: write - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python 3.9 - uses: actions/setup-python@v4 - with: - python-version: 3.9 - - - name: Install dependencies - run: | - pip install --user -U pip uv - uv sync - - - name: Build - run: | - uv pip install build - uv run python -m build - - - name: Publish - uses: pypa/gh-action-pypi-publish@release/v1 From 38dc715c47d2375ddb4ba465718384c3dbae0f73 Mon Sep 17 00:00:00 2001 From: alexeykozhevin Date: Fri, 20 Feb 2026 11:39:27 +0000 Subject: [PATCH 2/4] Bump version to 0.10.0 in pyproject.toml and uv.lock --- pyproject.toml | 2 +- uv.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6c253605d..31f014549 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "batchflow" -version = "0.9.4" +version = "0.10.0" description = "ML pipelines, model configuration and batch management" authors = [{ name = "Roman Kh", email = "rhudor@gmail.com" }] license = {text = "Apache License 2.0"} diff --git a/uv.lock b/uv.lock index 6d1728f9a..8c69f2118 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.12'", @@ -127,7 +127,7 @@ wheels = [ [[package]] name = "batchflow" -version = "0.9.4" +version = "0.10.0" source = { editable = "." } dependencies = [ { name = "dill" }, From 326e085d97b61d471c50708d873147c41c20cfd8 Mon Sep 17 00:00:00 2001 From: alexeykozhevin Date: Tue, 24 Feb 2026 13:46:19 +0400 Subject: [PATCH 3/4] Remove hardcoded cuda --- batchflow/models/torch/base.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/batchflow/models/torch/base.py b/batchflow/models/torch/base.py index 17826698a..6d24e7ea0 100755 --- a/batchflow/models/torch/base.py +++ b/batchflow/models/torch/base.py @@ -606,11 +606,16 @@ def _parse_devices(self): if devices is None: if torch.cuda.is_available(): self.device = torch.device('cuda:0') + elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available(): + self.device = torch.device('mps') else: self.device = torch.device('cpu') else: devices = devices if isinstance(devices, list) else [devices] - available_devices = [f'cuda:{i}' for i in range(torch.cuda.device_count())] + ['cpu'] + available_devices = [f'cuda:{i}' for i in range(torch.cuda.device_count())] + if hasattr(torch.backends, 'mps') and torch.backends.mps.is_available(): + available_devices.append('mps') + available_devices.append('cpu') for dev in devices: if isinstance(dev, torch.device): self.devices.append(dev) @@ -628,10 +633,11 @@ def _parse_devices(self): if device not in self.devices[:i]] self.device = self.devices[0] - if self.device.type == 'cpu': + if self.device.type != 'cuda': #TODO: maybe, we should add warning self.amp = False - torch.backends.cudnn.benchmark = config.get('benchmark', 'cuda' in self.device.type) + if torch.cuda.is_available(): + torch.backends.cudnn.benchmark = config.get('benchmark', self.device.type == 'cuda') def _parse_placeholder_shapes(self): """ Extract `inputs_shapes`, `targets_shapes`, `classes` from config. """ @@ -685,7 +691,7 @@ def make_infrastructure(self): self.make_loss() self.make_optimizer() self.make_decay() - self.scaler = torch.GradScaler("cuda") + self.scaler = torch.GradScaler(self.device.type) self.setup_gradient_clipping() self.setup_weights_averaging() @@ -884,7 +890,7 @@ def finalize_wa(self): self.model_to_device() self.make_optimizer() - self.scaler = torch.cuda.amp.GradScaler() + self.scaler = torch.GradScaler(self.device.type) self.wa_finalized = True @@ -1215,7 +1221,7 @@ def _train(self, inputs, targets, outputs_dict, sync_frequency, sam_rho, sam_ind targets = self.transfer_to_device(targets, non_blocking=True) # Compute predictions; store shapes for introspection - with torch.amp.autocast('cuda', enabled=self.amp): + with torch.amp.autocast(self.device.type, enabled=self.amp): predictions = self.model(inputs) # SAM: store grads from previous microbatches @@ -1223,7 +1229,7 @@ def _train(self, inputs, targets, outputs_dict, sync_frequency, sam_rho, sam_ind self._train_sam_store_gradients() # Compute loss and gradients; store loss value for every microbatch - with torch.amp.autocast('cuda', enabled=self.amp): + with torch.amp.autocast(self.device.type, enabled=self.amp): loss = self.loss(predictions, targets) loss_ = loss / sync_frequency @@ -1314,7 +1320,7 @@ def _train_sam_update_gradients(self, inputs, targets, sync_frequency, sam_rho, params_with_grads = [p + eps for p, eps in zip(params_with_grads, epsilons)] # Compute new gradients: direction to move to minimize the local maxima - with torch.amp.autocast('cuda', enabled=self.amp): + with torch.amp.autocast(self.device.type, enabled=self.amp): predictions_inner = self.model(inputs) loss_inner = self.loss(predictions_inner, targets) / sync_frequency (self.scaler.scale(loss_inner) if self.amp else loss_inner).backward() @@ -1470,7 +1476,7 @@ def _predict(self, inputs, targets, outputs_dict, amp, no_grad, transfer_from_de inputs = inputs[0] if len(inputs) == 1 and isinstance(inputs, list) else inputs targets = targets[0] if len(targets) == 1 and isinstance(targets, list) else targets - with (torch.no_grad() if no_grad else nullcontext()), torch.amp.autocast('cuda', enabled=amp): + with (torch.no_grad() if no_grad else nullcontext()), torch.amp.autocast(self.device.type, enabled=amp): inputs = self.transfer_to_device(inputs) predictions = self.model(inputs) From 191bd7e63d2d2e1283b7835eb08b395f65b254e2 Mon Sep 17 00:00:00 2001 From: Alexey Kozhevin Date: Tue, 24 Feb 2026 14:09:48 +0400 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- batchflow/models/torch/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/batchflow/models/torch/base.py b/batchflow/models/torch/base.py index 6d24e7ea0..0aa09e4e1 100755 --- a/batchflow/models/torch/base.py +++ b/batchflow/models/torch/base.py @@ -633,7 +633,7 @@ def _parse_devices(self): if device not in self.devices[:i]] self.device = self.devices[0] - if self.device.type != 'cuda': + if self.device.type == 'cpu': #TODO: maybe, we should add warning self.amp = False if torch.cuda.is_available():