<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by イルカ Borovec on Medium]]></title>
        <description><![CDATA[Stories by イルカ Borovec on Medium]]></description>
        <link>https://medium.com/@jborovec?source=rss-a89f75826628------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*Nlhuh10Ni5X6UKG9.</url>
            <title>Stories by イルカ Borovec on Medium</title>
            <link>https://medium.com/@jborovec?source=rss-a89f75826628------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 19 Jun 2026 05:20:03 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@jborovec/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[The Open-Source Shepherd: Why Your Best Work Is Building a Project That Doesn’t Need You]]></title>
            <link>https://medium.com/codex/the-open-source-shepherd-why-your-best-work-is-building-a-project-that-doesnt-need-you-bfd0641f5aca?source=rss-a89f75826628------2</link>
            <guid isPermaLink="false">https://medium.com/p/bfd0641f5aca</guid>
            <category><![CDATA[open-source]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[leadership]]></category>
            <category><![CDATA[community]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[イルカ Borovec]]></dc:creator>
            <pubDate>Thu, 11 Jun 2026 20:01:01 GMT</pubDate>
            <atom:updated>2026-06-11T20:01:01.027Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*p3ifnm8YzO2xTd75WbQ48A.png" /><figcaption>Generated illustration specifically for this blogpost</figcaption></figure><h4>There’s a path beyond just maintaining an open-source project: grow contributors, share ownership, and build a community that carries the project forward without you.</h4><p>There’s a moment — and if you’ve been maintaining an open-source project long enough, you know it — when you realize you’ve been doing this wrong. For me, it came during my time leading open-source at <a href="https://lightning.ai">Lightning AI</a>. The project had a creator who set the original vision, and I was one of the people steering the ship alongside them. But when I looked at the PR queue on a Monday morning — 20+ open pull requests, half of them waiting on one of maybe two or three people who could approve — the bottleneck was obvious. It wasn’t that the code was complex or that the architecture decisions required deep context. It was that we’d built a culture where almost everything funneled through a handful of gatekeepers.</p><p>We had contributors, sure. But the pattern was always the same: someone would show up, submit a PR for a specific itch they needed scratched, and disappear. The drive-by contribution. Valuable, but not sustainable. What we needed wasn’t more one-time contributors — it was retention. People who stuck around, who understood the codebase well enough to review others, who felt enough ownership to care about the next release, not just their own feature.</p><p>A good project is swamped with an active community and contributions. That’s the signal. But getting there means doing something that doesn’t come naturally to most technical people: investing in people as much as in code.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FhyOQ6bmuAmmAqt2CXC2Iw.png" /><figcaption>Generated illustrtaion specificly for this blogpost</figcaption></figure><h3>Maintainer vs. Shepherd</h3><p>The word “maintainer” is telling. It implies upkeep — keeping the lights on, fixing what breaks, patching the roof. And patching the roof will always be part of the job, for both the maintainer and the shepherd. That’s the emergency action, and someone has to do it. But the difference is in everything else you build <em>under</em> that roof. The maintainer patches. The shepherd builds a whole living space where others want to stay and contribute.</p><p>I’ve started thinking of the role as <strong>shepherding</strong>. Not in some grandiose sense — just the honest observation that the job is less about the code and more about the flock. A shepherd doesn’t carry every sheep. They set direction, keep things moving, watch for danger, and — critically — they raise other shepherds.</p><p>The traditional maintainer mindset says, “I know this codebase best, so I should make the important decisions.” The shepherd mindset asks: “How do I make sure this project has five people who can make these decisions, not just me?”</p><p>This isn’t a philosophical distinction. It’s a practical one, and it changes what you do on a daily basis.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PxHm2NI8Q-NznWifJk-DUQ.png" /><figcaption>Generated illustrtaion specificly for this blogpost</figcaption></figure><h3>The Four Duties</h3><p>Over the years — shepherding packages like <a href="https://github.com/Lightning-AI/torchmetrics">TorchMetrics</a> and <a href="https://github.com/Lightning-AI/pytorch-lightning">PyTorch Lightning</a>, building research tools like <a href="https://github.com/Borda/BIRL">BIRL</a>, and now working on projects like <a href="https://github.com/roboflow/rf-detr">RF-DETR</a> at <a href="https://roboflow.com">Roboflow</a> — I’ve come to see the role as four duties running in parallel. None of them is optional, and most maintainers only focus on the first two.</p><h4>1. Codebase Health</h4><p>This is the duty everyone recognizes. CI is green. Tests pass. Dependencies are current. Technical debt doesn’t spiral. You can look at the codebase on any given day, and it’s in a state where a new contributor could clone, build, and start reading without fighting the toolchain.</p><p>At Lightning, we invested heavily in ecosystem CI — automated compatibility testing across the dependency tree — because a broken downstream package is a broken trust relationship, even if it’s technically not your fault. I wrote about <a href="https://medium.com/pytorch-lightning/stay-ahead-of-breaking-changes-with-the-new-lightning-ecosystem-ci-b7e1cf78a6c7">this approach</a> separately, and later about <a href="https://medium.com/codex/build-a-custom-ci-bot-integrate-github-apps-with-lightning-for-fast-scalable-mlops-workflows-9a0f99b8a815">building a custom CI bot</a> on top of it, but the core insight is simple: codebase health isn’t just about your repo. It’s about every repo that depends on yours. I’ll be honest that some of the CI infrastructure involved private resources that I can’t fully disclose, but the principles — testing across your dependency tree, catching breaking changes before your users do — are universal and can be implemented with public tooling.</p><p>Here’s the part that connects to shepherding: if you’re the only person who can diagnose a CI failure, you haven’t solved the problem. You’ve just become the problem with a faster response time. Document your CI. Write runbooks. Make sure at least two other people can look at a red build and know what to do.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TzIwQZGVFBXFKyoK9vvvQA.png" /></figure><h4>2. Release Direction</h4><p>Someone has to decide what goes into the next release. Which features get prioritized? Which breaking changes are worth the migration cost? When to say “not now” to a perfectly good PR because it doesn’t fit the roadmap.</p><p>This is where things get nuanced. Some projects, by design, have a single point of approval for releases — especially when the project is tied to a company’s product or marketing cadence. You can debate whether that’s still a community project in the fullest sense, but it’s a reality many of us work within. That said, even within those constraints, there’s a spectrum. The traditional maintainer rarely allows any discussion about release scope. The shepherd makes their reasoning legible.</p><p>A public roadmap is one of the most powerful shepherding tools you have. Not a vague wish list — actual priorities with reasoning. When the direction is clearly given, the execution becomes a swarm. Contributors can self-select into work that matters. They can propose features with confidence that they’ll be evaluated against a known framework, not against the maintainer’s unpublished preferences.</p><p>When I was working on API deprecation strategies at Lightning (which eventually led to building <a href="https://github.com/Borda/pyDeprecate">pyDeprecate</a> — I wrote about the rationale <a href="https://medium.com/codex/mastering-api-deprecation-in-python-the-pain-points-and-how-pydeprecate-can-help-1dbfd90e2b62">in depth here</a>), the hardest part wasn’t the technical implementation. It was establishing a process transparent enough that contributors could propose deprecations themselves, with confidence they’d be reviewed fairly. pyDeprecate itself never got much public traction — it’s a narrow-scope, single-author utility — but the <em>process</em> it codified mattered far more than the tool.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0oup4LHZwh7G9i89hmJ3Mg.png" /></figure><h4>3. Mentorship</h4><p>This is where most projects fall short, and I’ll be the first to admit I was short on it in the early stages. I treated mentorship as a nice-to-have, something I’d get to when the “real” work was done. It took a while to realize that mentorship <em>is</em> the real work — and it’s an investment that snowballs fast. The time you spend teaching one contributor how to navigate the codebase pays off handsomely when they start reviewing PRs, answering issues, and onboarding the next person. The compounding is real.</p><p>Mentorship in OSS isn’t onboarding docs (though you need those). It’s the daily habit of reviewing code in a way that teaches, not just approves. It’s the difference between commenting “change this to X” and “here’s why X works better in this context, and here’s the pattern we follow across the codebase.” The first gets the PR merged. The second builds a contributor who doesn’t need you next time.</p><h4><strong>Concrete things that work:</strong></h4><ul><li><strong>Label issues honestly.</strong> “Good first issue” means something to newcomers. But also label the harder stuff — “needs design discussion” or “help wanted: experienced” — so growing contributors can find their next challenge. The worst thing you can do is leave the impression that only easy tasks are available for community members.</li><li><strong>Review with context, not just corrections.</strong> When someone submits a PR that works but doesn’t follow the project’s patterns, that’s a teaching moment, not a rejection. Show them the pattern. Explain the why. Link to another PR that solved something similar. This takes longer than a quick “LGTM” or a terse “please fix,” but it compounds.</li><li><strong>Give people real responsibility before you think they’re ready.</strong> I’ve seen contributors grow fastest when they were trusted with something slightly beyond their comfort zone — triaging a release, owning a module, being the point person on an issue area. Some of them stumbled. All of them learned faster than they would have by watching from the sidelines.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_xOB2s87LyU_0becjJjxsQ.png" /></figure><h4>4. Community Warmth</h4><p>This one sounds soft, but it’s load-bearing.</p><p>Open source runs on volunteer time. People contribute to projects where they feel welcomed, where their effort is acknowledged, and where disagreements are handled with respect. I’ve seen technically excellent projects bleed contributors because the issue tracker felt hostile, or because PRs sat unreviewed for weeks with no communication.</p><p>Community warmth isn’t about being nice for the sake of being nice. It’s about response time on first-time contributor PRs. It’s about saying “thank you for this, and here’s why we’re going a different direction” instead of closing an issue silently. It’s about writing release notes that credit contributors by name — as we do on <a href="https://github.com/roboflow/rf-detr/releases">RF-DETR</a>, where each release calls out new contributors and their specific additions.</p><p>Sometimes shepherding means carrying the last mile for a contributor’s PR. They’ve done 90% of the work, but life got in the way, or they’re stuck on a tricky edge case. The maintainer mindset says “stale, closing.” The shepherd mindset says, “let me finish this for you, because the contribution matters and the person should feel their effort wasn’t wasted.” That single act — picking up someone’s work and seeing it through — does more for retention than any amount of documentation.</p><p>At RF-DETR, we’ve seen this pattern play out. Community members have contributed everything from pose estimation support to bug fixes in evaluation metrics. Some of those PRs needed significant guidance. Some needed us to carry the finishing touches. The ones where we invested that effort? Those contributors came back. The ones where we let PRs go stale? They didn’t.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NcUhmrzn_uldun0iNNGTHg.png" /><figcaption>Generated illustrtaion specificly for this blogpost</figcaption></figure><h3>The Paradox: Shepherding Yourself Out of a Job</h3><p>Here’s the part that’s hard to internalize: the better you do this, the less essential you become. And that’s supposed to feel good.</p><p>I’ll be honest — it didn’t, at first. After years of being the person who knew every corner of the codebase, who could trace any bug to its origin, who held the institutional memory of every design decision, stepping back felt like losing something. There’s an identity wrapped up in being the person everyone turns to.</p><p>But here’s what I saw happen when I started actively working to make myself less necessary: the project got better. Not because I was bad at my job, but because five people with overlapping context make better decisions than one person with total context. They see blind spots you miss. They respond to issues in time zones you’re asleep in. They bring perspectives you don’t have.</p><p>The bus factor isn’t a joke metric. If you run git shortlog -sn on your repo and one name dominates every area, you have a fragile project, no matter how good that person is. The shepherd&#39;s duty is to make that distribution wider, not narrower.</p><p>I’ve carried this lesson from Lightning to <a href="https://roboflow.com">Roboflow</a>. When I look at a project now, I don’t ask “how much can I contribute?” I ask “how quickly can I build a team that can carry this without me?” Those are fundamentally different questions, and they lead to fundamentally different daily decisions.</p><h4>What This Looks Like in Practice</h4><p>Let me be concrete about the daily habits, because the philosophy is useless without them.</p><ul><li><strong>When reviewing a PR from a repeat contributor:</strong> Resist the urge to rewrite their code in your style. If it works and is readable, approve it. Save your energy for architectural feedback. The goal is to let their voice into the codebase, not to make every file look like you wrote it.</li><li><strong>When a new issue comes in that you could answer in 30 seconds:</strong> Wait. See if someone else picks it up. If they do, even if their answer isn’t perfect, let them finish. Jump in only to correct, not to override. This feels inefficient in the moment. Over months, it builds a team that handles triage without you.</li><li><strong>When planning a release:</strong> Write the plan in a public discussion or issue. Tag people who’ve contributed to relevant areas. Ask for input on priorities. The release will be slower the first few times. After that, it’ll be faster, because people understand the process and have opinions.</li><li><strong>When you’re about to take on a task yourself:</strong> Ask: “Is there someone who could do this with guidance?” If yes, guide them instead. Your duty isn’t to write the most code. It’s to make the most people productive.</li><li><strong>When a contributor’s PR goes quiet:</strong> Reach out. Ask if they need help. Offer to pair or to carry the last stretch. Don’t just close it after 30 days of silence. The person behind that PR might be your next core contributor — if you show them their work matters.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G7ms1UKzZpI9BQCt-m6lWA.png" /></figure><h3>Audit Your Project</h3><p>Open your project right now — the issue tracker, the PR queue, the last three releases — and ask yourself these questions.</p><p>If you disappeared tomorrow, who would merge the next PR? Who would cut the next release? If the answer is “nobody” or “it would stall,” that’s your signal.</p><p>Look at your contributor graph. Is it growing, flat, or shrinking? A project with a healthy shepherd has a contributor curve that trends upward independently of the shepherd’s own commit activity.</p><p>Check your issue response times. Not yours — everyone else’s. Are other people answering questions? Are they answering them well? If the community is silent except when you speak, you haven’t built community. You’ve built an audience.</p><p>Count the number of people who can approve a PR with confidence. If it’s fewer than three, you have work to do — and that work is more important than any feature on your roadmap.</p><p>The best open-source leaders I’ve known share a common trait: they’re proud of what happens when they’re not in the room. Their legacy isn’t the clever algorithm or the elegant API. It’s the contributor who became a co-maintainer. It’s the process that runs smoothly without oversight. It’s the project that thrives precisely because it was built to not depend on any single person.</p><p>That’s what shepherding means. Not writing the most code. Not making the most decisions. Building something that outlives your involvement — and having the honesty to measure yourself by that standard.</p><p>If this resonates with you — if you’re wrestling with these questions in your own project — I’d love to hear from you. Reach out, share your experience, or just start a conversation. Sometimes the hardest part of shifting from maintainer to shepherd is knowing you’re not the only one figuring it out.</p><p>You can find my work and projects on <a href="https://github.com/Borda">GitHub</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bfd0641f5aca" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codex/the-open-source-shepherd-why-your-best-work-is-building-a-project-that-doesnt-need-you-bfd0641f5aca">The Open-Source Shepherd: Why Your Best Work Is Building a Project That Doesn’t Need You</a> was originally published in <a href="https://medium.com/codex">CodeX</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What Is Your Python Package’s Public API, Really?]]></title>
            <link>https://medium.com/codex/what-is-your-python-packages-public-api-really-3ff64a75bf7f?source=rss-a89f75826628------2</link>
            <guid isPermaLink="false">https://medium.com/p/3ff64a75bf7f</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[open-source]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[api-design]]></category>
            <dc:creator><![CDATA[イルカ Borovec]]></dc:creator>
            <pubDate>Thu, 19 Feb 2026 16:41:01 GMT</pubDate>
            <atom:updated>2026-02-19T16:41:01.306Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M7RGZVpc3V6fl4lmXJK0qQ.png" /><figcaption>Generated ilustration for public/private API.</figcaption></figure><h4><em>The answer isn’t black or white — and reality shifts depending on how many people use your package. That’s the real challenge.</em></h4><p>If you maintain a Python package, you’ve probably asked yourself: <strong>what exactly counts as my public API?</strong> Unlike Java or C++, Python doesn’t have public or private keywords. We just have conventions — underscores, __all__, documentation — which are basically gentlemen&#39;s agreements. But here&#39;s the thing: this seemingly abstract question has real consequences for how you work.</p><p>Here’s what I mean: sure, you <em>can</em> change anything you want — it’s your code. But the fallout is completely different depending on what you touch. Rename a private helper? Nobody even notices. Rename a public function without warning? Get ready for bug reports, frustrated users, and the worst kind of damage: people quietly deciding your package is too unpredictable and moving on.</p><p>Anything that’s actually public needs a proper <strong>deprecation cycle</strong>: warn users, give them time to adapt, then remove it. That’s extra releases, extra coordination, extra work. So when I ask <em>“what is public?”</em> I’m really asking, <em>“what can’t I just change on a whim?”</em> And in Python, that’s way less obvious than you’d think.</p><p>I’m going to walk through the three ways Python lets you define a public API, explain why they’re really a spectrum that depends on how many people use your code, and share a mental model I’ve found helpful for thinking about API boundaries.</p><h3>What Does Python Actually Say?</h3><p>Before we get into the messy reality, let’s check what Python officially recommends. <a href="https://peps.python.org/pep-0008/#public-and-internal-interfaces"><strong>PEP 8</strong></a> — the style guide everyone references — actually has a whole section on “Public and Internal Interfaces.” Here’s what it says:</p><blockquote>“Any backwards compatibility guarantees apply only to public interfaces.”</blockquote><blockquote>“Documented interfaces are considered public, unless the documentation explicitly declares them to be provisional or internal.”</blockquote><blockquote>“All undocumented interfaces should be assumed to be internal.”</blockquote><blockquote>“To better support introspection, modules should explicitly declare the names in their public API using the <strong>all</strong> attribute.”</blockquote><blockquote>“Internal interfaces should still be prefixed with a single leading underscore.”</blockquote><p>So PEP 8 mentions all three mechanisms — documentation, __all__, and underscores — but treats them as complementary layers, not pick-one-or-the-other options. In a perfect world, everyone would follow this to the letter, and there&#39;d be no confusion. In reality? Most packages use bits and pieces of these, and that&#39;s where things get messy.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PoVbKJcJH4ophtPWueNuig.png" /></figure><h3>The Three Levels of “Public” (In Reality)</h3><p>Python doesn’t actually enforce any of this. Most packages lean heavily on one mechanism and barely touch the others. Let me walk through them from strictest to most permissive.</p><h4>Level 1: Documented in Docs Only</h4><p>PEP 8 says: <em>documented interfaces are public.</em> This is the clearest possible boundary — if it’s not in the docs, it doesn’t exist. But it’s completely an honor system. Python won’t stop anyone from importing undocumented stuff.</p><p>The bigger problem? Documentation has a reputation issue. Many developers see docs as training wheels for people who can’t read code. They’ll go straight to your source, browse through the modules, and use whatever looks useful. And let’s be honest — docs drift. That function you refactored three months ago? The docs probably still describe the old signature. Sure, you can auto-generate docs from docstrings, but how many times have you revisited what was once documented as public and is now effectively private? The code changes, the docstrings get updated (maybe), but that overview page listing “Public APIs”? Yeah, nobody touched that in two years. So while “documented = public” is the gold standard in theory, in practice, it’s the weakest enforcement mechanism you have.</p><h4>Level 2: Exported via init.py and all</h4><p>A step closer to enforcement. PEP 8 recommends that modules explicitly declare their public API using <a href="https://realpython.com/python-all-attribute/">__all__</a>, and that imported names should be considered implementation details unless explicitly re-exported. The __all__ variable has a concrete runtime effect: it controls what from package import * pulls in. But that&#39;s all it gates — a user can still write from your_package.internal_module import something, and Python won&#39;t complain.</p><p>But here’s where it gets practical: in the real world (and if you check the docs for most well-maintained packages), the recommendation is actually to use from package import SpecificThing instead of import package or the wildcard. Why? You don&#39;t want to import all the unused ballast functions, and in many cases, you&#39;re avoiding the import of heavy dependency packages that get loaded when you import the whole module. So , __all__ and a clean __init__.py aren&#39;t just about API boundaries — they&#39;re about performance and keeping your import times reasonable.</p><h4>Level 3: Anything Without a Leading Underscore</h4><p>This is what actually happens in the wild. Python says names starting with _ are private. Everything else? Fair game. (Even CPython itself struggles with this — <a href="https://peps.python.org/pep-0689/">PEP 689</a> had to formalize what &quot;unstable&quot; means for the C API, illustrating the same tension at the language level.) Users browsing your code, hitting tab-complete, or checking dir() will see every non-underscored name and assume it&#39;s meant for them. This is the weakest definition — but it&#39;s what your users fall back on when you haven&#39;t been clear about the other two.</p><h3>The Adoption Gradient: When Stuff Starts Breaking</h3><p>Here’s the part that caught me off guard when I started maintaining packages: <strong>your public API isn’t defined by your conventions. It’s defined by how many people use your code.</strong> The more users you have, the bigger the blast radius when you change anything.</p><p>There’s actually a name for this — <a href="https://www.hyrumslaw.com/"><strong>Hyrum’s Law</strong></a> (from a Google engineer):</p><blockquote><em>“With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.”</em></blockquote><p>Python makes this worse because there’s literally nothing stopping people. No access modifiers, no compiler errors, no sealed modules. A leading underscore is just a polite “hey, maybe don’t use this.” When someone discovers that your_package.utils._parse_header() does exactly what they need, they&#39;ll use it, build their workflow around it, and file an issue when you rename it.</p><p>What ends up happening: as your user base grows, your <em>intended</em> API and your <em>actual</em> API drift apart. You can still change whatever you want — but every name people depend on is another landmine. The trick is figuring out which names people are actually using.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tlB8iQIXU09UBrr97-MXpg.png" /><figcaption>Generated illustration for the followig section.</figcaption></figure><h3>My Take: Three Tiers of Package Maturity</h3><p>So what do you actually do about this? After working on my own packages and contributing to projects at different scales, I’ve settled on a simple mental model. It’s not a rule — just something that’s helped me figure out how strict I need to be at different stages.</p><h4>Tier 1: The Home Project — Public = What’s in the Docs</h4><p>When your package is basically a personal tool or a research project with maybe a dozen users, you’re in the Wild West. You probably know most of your users personally, and nobody’s expecting rock-solid stability. Think internal lab tools, proof-of-concept implementations, or that utility script that grew legs and became a package. If I needed to rename some internal function, I’d just do it and mention it in the changelog. To be honest, at this stage, there often isn’t much documentation. The “public API” is whatever made sense at the time — if it worked for the research paper or the specific use case, it stayed. Being super strict about API boundaries would’ve just slowed things down for no real benefit. At this stage, you’re optimizing for velocity, not compatibility.</p><h4>Tier 2: The End-User Package — Public = What’s in init</h4><p>Once people are installing your package from PyPI and using it directly, expectations change. They’re doing from your_package import SomeClass and expecting it to work across updates. Think <a href="https://github.com/Borda/pyDeprecate">pyDeprecate</a> or <a href="https://github.com/python-cachier/cachier">cachier</a> — tools people deliberately install and build into their code. At this stage, you start formalizing documentation. You’re writing proper guides, listing what’s public, and users actually expect that information to be there. Sometimes you define __all__ to make the boundaries explicit, sometimes you just keep __init__.py clean and let that do the talking. It helps IDEs autocomplete properly and makes your intent obvious. You&#39;re no longer just optimizing for velocity — you&#39;re balancing that with some stability guarantees.</p><h4>Tier 3: The Dependency Package — Public = Everything without _</h4><p>This is when you become the backbone for other people’s work. Other packages list you in their requirements.txt and users who&#39;ve never heard of you get affected by your releases. <a href="https://github.com/Lightning-AI/pytorch-lightning">PyTorch Lightning</a> and <a href="https://github.com/Lightning-AI/torchmetrics">TorchMetrics</a> are in this category — thousands of downstream packages depend on them. At this point, you need to be aware that a single change can ripple through systems you never imagined your work would be used in. Someone built a production data pipeline around your package. Another team is using it in medical imaging software. A startup has it deep in its infrastructure. You find out about these use cases when something breaks, not before. At this scale, it doesn’t matter that you only documented five functions. If a hundred packages are importing some helper function because it’s convenient and doesn’t have an underscore, removing it <em>is</em> a breaking change. <strong>Every non-underscored name is now a promise.</strong></p><blockquote>You know how people install the entire scikit-learn package just to use train_test_split? That&#39;s the reality — users will depend on whatever is convenient, not just what you documented.</blockquote><p>This is exactly why we built <a href="https://devblog.pytorchlightning.ai/stay-ahead-of-breaking-changes-with-the-new-lightning-ecosystem-ci-b7e1cf78a6c7">Ecosystem CI</a> — to run downstream tests against every nightly build and catch these breaks before shipping.</p><h3>What This Actually Means Day-to-Day</h3><p>The bottom line: you can change whatever you want — it’s your code. But changing stuff people treat as public without warning means angry users, broken builds, or the slow death of trust that comes from being “that package that keeps breaking stuff.” The responsible move is deprecation: warn people, give them time, then remove it. That’s real work. And in Python, where “public” is fuzzy, you often don’t know upfront how much pain you’re signing up for.</p><h4>Some habits that help:</h4><ol><li><strong>Define all early.</strong> It’s basically free and makes your intentions clear. Way easier to add names later than to yank something people already depend on.</li><li><strong>Use underscores liberally.</strong> Any function, class, or module that’s not meant for users should start with _. It&#39;s the clearest signal Python gives you.</li><li><strong>Keep init.py minimal.</strong> Only re-export what you actually want people using. Don’t do from .utils import * — it leaks internal names everywhere.</li><li><strong>Be explicit in docs.</strong> Add a “Public API” section (see <a href="https://mkdocstrings.github.io/griffe/guide/users/recommendations/public-apis/">Griffe’s recommendations</a> for a solid template). <a href="https://semver.org/">SemVer</a> requires you to declare what’s public anyway — the clearer you are, the more your version numbers actually mean something.</li><li><strong>Make deprecation easy on yourself.</strong> Deprecation is inevitable for any evolving package. Tools like <a href="https://medium.com/codex/mastering-api-deprecation-in-python-the-pain-points-and-how-pydeprecate-can-help-1dbfd90e2b62">pyDeprecate</a> make it painless — one decorator, automatic forwarding, warnings that don’t spam logs, argument remapping. When it’s that easy, you stop avoiding necessary changes.</li></ol><h3>Bottom Line: Your Users Define Your API</h3><p>Python’s flexibility cuts both ways. The “we’re all adults here” philosophy makes the language great to use, but it also means your internal implementation is one import away from becoming someone’s hard dependency. The earlier you accept this and set clear boundaries, the fewer nasty surprises you’ll hit when you need to refactor.</p><h4><strong>Here’s the reality:</strong></h4><ul><li>Small project? Your docs are your API</li><li>People installing directly? Your __init__ is your API</li><li>Do other packages depend on you? Everything without an underscore is your API, like it or not</li></ul><p>Know which tier you’re in, and you’ll know how careful you need to be — and whether that “quick refactor” is going to be silent or heard across the entire ecosystem.</p><blockquote>Ever had your API boundaries bite you? I’d love to hear about it in the comments.</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3ff64a75bf7f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codex/what-is-your-python-packages-public-api-really-3ff64a75bf7f">What Is Your Python Package’s Public API, Really?</a> was originally published in <a href="https://medium.com/codex">CodeX</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to organize a medical imaging challenge: Lessons from ANHIR]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/data-science-collective/how-to-organize-a-medical-imaging-challenge-lessons-from-anhir-15fd486adbf2?source=rss-a89f75826628------2"><img src="https://cdn-images-1.medium.com/max/1024/1*cn583IB7KUXyKCadqRF6DQ.jpeg" width="1024"></a></p><p class="medium-feed-snippet">A practical guide to running an academic challenge, from conception to publication, including mistakes I made so you don&#x2019;t have to.</p><p class="medium-feed-link"><a href="https://medium.com/data-science-collective/how-to-organize-a-medical-imaging-challenge-lessons-from-anhir-15fd486adbf2?source=rss-a89f75826628------2">Continue reading on Data Science Collective »</a></p></div>]]></description>
            <link>https://medium.com/data-science-collective/how-to-organize-a-medical-imaging-challenge-lessons-from-anhir-15fd486adbf2?source=rss-a89f75826628------2</link>
            <guid isPermaLink="false">https://medium.com/p/15fd486adbf2</guid>
            <category><![CDATA[medical-imaging]]></category>
            <category><![CDATA[challenge]]></category>
            <category><![CDATA[organizing]]></category>
            <category><![CDATA[lessons-learned]]></category>
            <category><![CDATA[deployment]]></category>
            <dc:creator><![CDATA[イルカ Borovec]]></dc:creator>
            <pubDate>Wed, 05 Nov 2025 19:47:15 GMT</pubDate>
            <atom:updated>2025-11-06T09:18:33.856Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[BIRL: Benchmarking Image Registration Through Landmarks]]></title>
            <link>https://pub.towardsai.net/birl-benchmarking-image-registration-through-landmarks-1bc1c8ab671a?source=rss-a89f75826628------2</link>
            <guid isPermaLink="false">https://medium.com/p/1bc1c8ab671a</guid>
            <category><![CDATA[medical-imaging]]></category>
            <category><![CDATA[landmark]]></category>
            <category><![CDATA[challenge]]></category>
            <category><![CDATA[histology-and-imaging]]></category>
            <category><![CDATA[image-registration]]></category>
            <dc:creator><![CDATA[イルカ Borovec]]></dc:creator>
            <pubDate>Fri, 24 Oct 2025 15:02:01 GMT</pubDate>
            <atom:updated>2025-10-24T15:02:01.899Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VpHYS1QyQTBK-aqTxHKtVQ.jpeg" /><figcaption>generated ilustration for thsi post</figcaption></figure><h3>The ANHIR Challenge Problem</h3><p>When the organizers of the ANHIR challenge at ISBI 2019 set out to compare automatic histological image registration methods, they faced a fundamental problem: how do you fairly evaluate algorithms that align tissue images with wildly different staining protocols, deformations, and artifacts? The answer became <a href="https://borda.github.io/BIRL/">BIRL</a> — a Python framework that turns the messy process of benchmarking into something systematic and reproducible.</p><p>The challenge itself was ambitious. Teams worldwide submitted methods to register 355 histological images spanning eight tissue types with eighteen different stains. Some images showed lung tissue with H&amp;E staining, others displayed kidney sections with immunohistochemistry markers, and still others presented breast tissue with completely different protocols. The tissue could be torn, folded, or distorted during preparation. Staining intensity varied unpredictably. Yet somehow, algorithms needed to find correspondence between these radically different appearances of the same underlying anatomy.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*6iP_t3LIeQ58yXuH.jpg" /><figcaption>Sample images from aet d</figcaption></figure><h3>Landmarks as the Universal Metric</h3><p>BIRL’s solution was elegantly simple: use manually annotated landmarks — at least forty per image pair, ideally distributed uniformly across the tissue — and measure how far apart corresponding points end up after registration. This uniform distribution ensures the landmarks represent deformations across the entire tissue rather than clustering in easy-to-annotate regions. The Target Registration Error became the universal currency for comparing methods. When you normalize it by the image diagonal, you get a percentage that works across datasets of any size. The best methods in ANHIR achieved median errors around 0.44% of the image diagonal, successfully registering over 98% of landmark pairs across the entire dataset.</p><p>What made this possible was BIRL’s design philosophy. Rather than forcing researchers into rigid workflows, the framework provides base classes that new methods can inherit from with minimal overhead. Want to test your novel registration algorithm? Override a couple of methods, specify your parameters in a YAML file, and run. The framework handles parallel execution across your dataset, checkpoints progress so you can resume after interruptions, and generates visualizations showing where alignments succeed or fail. It’s the kind of infrastructure that disappears into the background once you understand it, letting you focus on the actual registration problem.</p><h4>Integrated Methods and What ANHIR Revealed</h4><p>The framework came bundled with wrappers for established methods that represented the state of the art in 2019. There’s bUnwarpJ from the ImageJ ecosystem, elastix from the ITK medical imaging toolkit, ANTs with its sophisticated diffeomorphic transforms, and several others. Each integration shows you how to configure parameters, invoke the registration, and extract results in BIRL’s expected format. Running a benchmark becomes as straightforward as pointing to a CSV file describing your image pairs and launching the script.</p><p>However, the real insight from ANHIR — and what BIRL enabled researchers to discover — was the significant impact of domain adaptation. Off-the-shelf methods, configured for general medical imaging, consistently underperformed compared to approaches tailored for histological challenges. The winning methods employed multi-resolution strategies, starting with coarse alignment before refining at higher resolutions. They used robust initial registration steps to handle the extreme appearance differences between differently stained tissues. These weren’t necessarily novel algorithms; they were careful orchestrations of existing techniques, and BIRL made this kind of systematic comparison possible.</p><h3>The Registration Landscape in 2025</h3><p>Since the ANHIR challenge concluded and BIRL’s active development ended in 2020, the registration landscape has evolved considerably. As an academic project tied to a specific challenge, BIRL’s archival was natural and expected. Several tools have emerged or matured since then, many exceeding BIRL’s capabilities in specific domains while building on the evaluation principles it established:</p><ul><li><strong>VALIS</strong> (2021–present): Production-ready automated solution for whole slide images with groupwise alignment and modern feature detectors (DISK, DeDoDe), handles multi-gigapixel files effectively but is primarily CPU-based and may struggle with severely deformed tissues. Published in Nature Communications 2023, actively maintained with regular updates.</li><li><strong>MMIR</strong> (2024–present): Cloud-based platform with plugin architecture and web GUI for collaborative clinical research, offering scalability and easy algorithm integration, though requiring Docker setup and potential custom development for specialized modalities. Published in BMC Medical Informatics and Decision Making in March 2024, represents a recent advancement in multimodal histological registration.</li><li><strong>HistoReg</strong> (2021–present): Specialized for sequential variably-stained histology with integrated ANHIR evaluation, automatically processes various file formats but requires compilation with external dependencies, and is restricted to research use. Published in Applied Sciences 2021, actively maintained by CBICA.</li><li><strong>Continuous Registration Challenge</strong> (2019–present): Maintains ongoing leaderboards for organ-specific datasets with standardized evaluation protocols, promoting reproducible progress but limited to predefined datasets and requiring code submissions rather than local experimentation. Established alongside ANHIR and continues as an active community resource.</li><li><strong>elastix / SimpleElastix</strong> (2003–present): Highly configurable ITK-based toolkit with extensive transform options and proven performance across medical imaging domains, though the learning curve is steep and it lacks integrated benchmarking workflows. Elastix was first developed 2003, published in IEEE TMI 2010; SimpleElastix was added 2015. Actively maintained with regular releases, moved to GitHub 2017, and remains the gold standard for medical image registration.</li><li><strong>AROSICS</strong> (2017–present): Optimized for satellite and multi-sensor remote sensing data with robust subpixel shift detection, efficiently handling global and local misalignments, but poorly suited to histological or medical applications due to different imaging assumptions. Published in Remote Sensing 2017, actively maintained with updates.</li></ul><p>BIRL sits among these as the benchmark’s tool from a specific era. While no longer actively developed, it established patterns for rigorous evaluation that these newer tools build upon. Many of these alternatives now offer capabilities — automated pipelines, cloud infrastructure, modern feature detectors — that weren’t priorities or even available when BIRL was designed for the 2019 challenge.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*GOkQxLrPOvUHc6zK.jpg" /><figcaption>Visualization of Target error</figcaption></figure><h3>Why BIRL Still Matters</h3><p>The repository was archived in 2023, but that doesn’t diminish its value. The code is open-source under a BSD license, ready for forking and adaptation. The architectural patterns it established — landmark-based validation, modular method integration, comprehensive evaluation metrics — remain relevant even as the methods being evaluated evolve. Deep learning approaches to registration are emerging rapidly, and they need benchmarking infrastructure just as much as classical optimization methods did.</p><p>What makes BIRL particularly valuable is its approach to controlled experimentation. You can generate synthetic data with known deformations, vary staining appearance, and inject artifacts — all to stress-test your method before running it on precious real data. This complements the landmark-based evaluation on real images, where ground truth is approximated through manual annotation. The framework handles the tedious infrastructure: CSV-based dataset configuration, Docker images for reproducible environments, performance profiling utilities, parallel execution with checkpointing, and automated visualization of alignment quality.</p><p>Perhaps most importantly, BIRL encoded the lessons learned from a major community challenge into reusable infrastructure. The ANHIR results, published in IEEE Transactions on Medical Imaging, showed that successful registration requires more than just implementing the latest algorithm. You need robust initialization, appropriate regularization, multi-scale strategies, and careful handling of the specific artifacts your imaging modality introduces. Having a framework that lets you systematically test these design decisions against quantitative metrics changes how you approach the registration problem.</p><h4>Looking forward</h4><p>The field keeps moving forward with transformer-based feature detectors, optical flow networks, and physics-informed deformation models. All of these developments benefit from standardized evaluation frameworks that make progress measurable rather than anecdotal. For researchers starting new registration projects, BIRL’s annotation protocols and baseline method implementations provide a solid foundation. You can quickly establish whether your problem is actually solvable — whether there are consistent anatomical features to match across your imaging conditions — before investing in sophisticated solutions. The challenge dataset with its 355 carefully annotated image pairs remains a valuable resource, representing diverse registration scenarios from rigid alignment of serial sections to handling tears and folds in multi-modal correspondence.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*t7MOv8xDsFhOhAkL.jpg" /></figure><h4>A Framework That Balances</h4><p>Looking at BIRL now, years after the challenge concluded, what stands out is how it balanced generality with specificity. General enough to accommodate diverse registration methods and evaluation scenarios. Specific enough to enforce practices — like minimum landmark counts and standardized error metrics — that make results comparable. This balance is rare in research software, which tends to drift toward either overly abstract frameworks that don’t do anything concrete or overly specific tools that can’t adapt to new problems.</p><p>The repository at <a href="github.com/Borda/BIRL">github.com/Borda/BIRL</a> contains everything needed to start benchmarking: sample datasets, integration templates for multiple methods, preprocessing utilities, evaluation scripts, and comprehensive documentation. While archived, it remains fully functional, and its BSD license welcomes derivatives. Whether you’re evaluating a novel registration algorithm, comparing existing methods on your dataset, or teaching computational approaches to medical imaging, BIRL provides tested infrastructure that handles the tedious parts while staying out of your way.</p><p>The broader registration ecosystem continues its evolution, but the fundamental question BIRL addresses remains constant: how do we know if one method is actually better than another? Answering that question rigorously, with standardized protocols and quantitative metrics, is what transforms registration from an art into engineering. The framework that enabled 256 teams to compete fairly in ANHIR is ready for whatever registration challenges come next.</p><blockquote><em>If you want to learn more, reach out on </em><a href="https://www.linkedin.com/in/jirka-borovec/"><em>LinkedIn</em></a></blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1bc1c8ab671a" width="1" height="1" alt=""><hr><p><a href="https://pub.towardsai.net/birl-benchmarking-image-registration-through-landmarks-1bc1c8ab671a">BIRL: Benchmarking Image Registration Through Landmarks</a> was originally published in <a href="https://pub.towardsai.net">Towards AI</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Build a Custom CI Bot: Integrate GitHub Apps with Lightning for Fast, Scalable MLOps Workflows]]></title>
            <link>https://medium.com/codex/build-a-custom-ci-bot-integrate-github-apps-with-lightning-for-fast-scalable-mlops-workflows-9a0f99b8a815?source=rss-a89f75826628------2</link>
            <guid isPermaLink="false">https://medium.com/p/9a0f99b8a815</guid>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[github-app]]></category>
            <category><![CDATA[mlops]]></category>
            <category><![CDATA[continuous-integration]]></category>
            <category><![CDATA[automation]]></category>
            <dc:creator><![CDATA[イルカ Borovec]]></dc:creator>
            <pubDate>Mon, 22 Sep 2025 09:27:46 GMT</pubDate>
            <atom:updated>2025-09-22T09:27:46.511Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3odc2Zfs5WyBzskbYkqr8Q.png" /><figcaption>AI generated illustration for this post</figcaption></figure><h3>Build a Custom CI Bot: Integrate GitHub Apps with Lightning for Faster, Scalable Development and MLOps Workflows</h3><p>In the fast-paced world of software development, especially in machine learning and data-intensive projects, continuous integration (CI) is essential for maintaining code quality and accelerating iteration cycles. While standard CI platforms like GitHub Actions, Jenkins, or CircleCI provide solid foundations, they often have inherent limitations that can restrict advanced workflows. In this post, I introduce a lightweight GitHub App bot that bridges GitHub’s ecosystem with Lightning’s powerful compute platform. We’ll explore why it’s worth trying, the benefits it brings to development and MLOps lifecycles, a brief overview of its architecture and task lifecycle, and three unique design choices that make it stand out.</p><h3>Limitations of Standard CI Platforms</h3><p>Traditional CI systems excel at basic testing and deployment, but can struggle with the need for high computational power and flexibility. Common pain points include:</p><ul><li><strong><em>Limited Hardware Options</em></strong>: Most platforms offer only basic CPU runners, with limited access to specialized GPUs like NVIDIA A100, V100, or T4 — critical for ML workloads requiring GPU acceleration for training and inference tests.</li><li><strong><em>Cost Inefficiencies</em></strong>: While some CI platforms offer pay-as-you-go pricing models, these options tend to be significantly costlier than prepaid plans and usually do not provide access to cheap spot instances. Spot instances can reduce costs by up to 90%, but standard CI systems rarely support them or handle their preemption risks effectively.</li><li><strong><em>Scalability Constraints</em></strong>: Parallelism is often capped, making large-scale testing across multiple environments (different OS, Python versions, hardware configurations) cumbersome and costly.</li><li><strong><em>Customization Gaps</em></strong>: Repository-specific rules or dynamic workflows triggered by webhook events often require complex setups, increasing maintenance overhead.</li></ul><p>These limitations can slow development, especially in MLOps, where iterative experimentation on diverse compute environments is vital.</p><h3>How This Custom CI Bot Enhances Development and MLOps</h3><p>This custom bot connects GitHub Apps directly to Lightning’s distributed compute platform, delivering a more tailored CI experience. Acting as a bridge, it lets you run automated pull request (PR) checks with repository-specific rules powered by scalable cloud resources. Benefits include:</p><ul><li><strong>Enhanced Resource Access</strong>: Instantly access a broad range of GPU types and configurations on demand. For MLOps, this means seamless incorporation of model training or validation steps into your CI pipeline without owning infrastructure.</li><li><strong>Cost Optimization with Spot Instances</strong>: Lightning’s support for spot instances enables economical job runs. The bot intelligently handles preemptions by retrying failed tasks, ensuring reliability without overspending — ideal for non-critical tests or large batch jobs.</li><li><strong>Faster Iterations and Scalability</strong>: Distributed execution across multiple environments speeds feedback loops. In development, this accelerates PR reviews; in MLOps, it supports end-to-end pipelines from data processing to deployment, reducing time from commit to production.</li><li><strong>Custom Automation</strong>: Workflows are defined via simple YAML files triggered by GitHub webhook events (e.g., PR opens, pushes), promoting best practices like automated code quality checks, multi-environment testing, and secure repository access that streamline team collaboration.</li><li><strong>Pay-Per-Use Efficiency</strong>: You pay only for what you use, aligning costs with actual workload rather than fixed quotas. This model suits open-source projects and startups scaling MLOps without heavy upfront investments.</li></ul><p>Overall, this approach transforms CI from a checkbox task into a strategic enabler that tackles compute bottlenecks and fosters robust development and MLOps practices.</p><h3>Architecture and Task Lifecycle</h3><p>The bot’s architecture emphasizes simplicity and scalability, using Python core logic integrated with event-driven components.</p><h4>High-Level Architecture</h4><ul><li><strong>GitHub App Integration</strong>: The bot registers as a GitHub App with secure, scoped repository access. It listens for webhooks signaling events such as PR creation or updates.</li><li><strong>Task Queuing with Redis</strong>: Incoming events are queued in Redis, a fast in-memory data store that acts as a message broker. This allows asynchronous processing and load balancing among workers.</li><li><strong>Worker Processes</strong>: Python-based workers pull tasks from Redis and execute them by interfacing with Lightning’s platform to spin up compute jobs.</li><li><strong>Lightning Compute Layer</strong>: Jobs run on Lightning’s distributed cloud, supporting multi-node setups, GPU acceleration, and spot instances. Results are collected and reported.</li><li><strong>Feedback Loop</strong>: Status updates (pass/fail) are posted back to GitHub PRs as comments or checks.</li></ul><h4>Task Lifecycle</h4><ol><li><strong><em>Trigger</em></strong>: A GitHub event (e.g., PR open, push commit) triggers a webhook sent to the bot.</li><li><strong><em>Processing and Queuing</em></strong>: The bot validates the event against repository-specific YAML configurations, then queues the task in Redis with environment specifications and commands.</li><li><strong><em>Execution</em></strong>: A worker dequeues the task, authenticates with Lightning, provisions resources (e.g., GPU spot instances), and runs workflows like testing across Python versions on Ubuntu with CUDA.</li><li><strong><em>Completion and Reporting</em></strong>: Results are aggregated and posted back to the PR. Failures may trigger retries or notifications.</li><li><strong><em>Cleanup</em></strong>: Resources are released efficiently.</li></ol><p>This design ensures low-latency responses and adapts to varying loads, serving both small and large-scale projects effectively.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*RVq4Rb9jc0862pHo.jpg" /><figcaption>Demo use-case</figcaption></figure><h3>Three Design Highlights</h3><p>Here are three design choices that demonstrate the bot’s practical value:</p><ul><li><strong>Event-Driven YAML Workflows</strong>: Instead of hardcoding logic, workflows are defined dynamically in repo-specific YAML files. This enables flexible rules (e.g., run GPU tests only for ML-related changes), reducing boilerplate and easing customization without redeployments.</li><li><strong>Redis for Decoupled Workers</strong>: Redis decouples event handling from execution, enabling horizontal worker scaling and fault tolerance — if a worker crashes, tasks remain queued. This integrates smoothly with Lightning’s job scheduling for resilient MLOps pipelines.</li><li><strong>Secure, Pay-Per-Use Lightning Integration</strong>: The bot uses GitHub App tokens for authentication and Lightning’s API for on-demand compute. This approach minimizes security risks (no stored credentials) and aligns costs with usage via spot instances, while providing traceability through GitHub checks.</li></ul><p>If you want to supercharge your CI without the complexity of full-fledged platforms, try this bot — clone the repo at <a href="https://github.com/Borda/GitHub-app-bot">https://github.com/Borda/GitHub-app-bot</a> and set it up in minutes. Feedback and contributions are welcome! What custom CI tweaks have you tried in your projects?</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9a0f99b8a815" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codex/build-a-custom-ci-bot-integrate-github-apps-with-lightning-for-fast-scalable-mlops-workflows-9a0f99b8a815">Build a Custom CI Bot: Integrate GitHub Apps with Lightning for Fast, Scalable MLOps Workflows</a> was originally published in <a href="https://medium.com/codex">CodeX</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Mastering API Deprecation in Python: The Pain Points and How pyDeprecate Can Help]]></title>
            <link>https://medium.com/codex/mastering-api-deprecation-in-python-the-pain-points-and-how-pydeprecate-can-help-1dbfd90e2b62?source=rss-a89f75826628------2</link>
            <guid isPermaLink="false">https://medium.com/p/1dbfd90e2b62</guid>
            <category><![CDATA[deprecation]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[api]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[イルカ Borovec]]></dc:creator>
            <pubDate>Sun, 21 Sep 2025 10:32:05 GMT</pubDate>
            <atom:updated>2026-06-03T20:15:25.844Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OCLla2-b8_FvwiiEDbBEvg.png" /><figcaption>Illustratiion generated for this post.</figcaption></figure><h4><em>Evolving an API doesn’t have to break your users — forward old calls, remap arguments, and enforce removal dates with a single decorator.</em></h4><p>In the ever-evolving world of software development, change is inevitable. Libraries and frameworks must adapt to new requirements, improved architectures, or external dependencies. However, this evolution often comes with a hidden cost: managing deprecated APIs. As a maintainer, you want to push your codebase forward, but you can’t afford to alienate users by breaking backward compatibility overnight. This tension creates real pain points that can slow down progress and increase maintenance overhead. In this post, we’ll unpack these challenges and introduce <a href="https://borda.github.io/pyDeprecate">pyDeprecate</a>, a simple yet powerful Python package that streamlines the deprecation process through elegant wrappers. Whether you’re a library author or contributor, understanding this value framework can transform how you handle API transitions.</p><h3>The Pain Points of API Deprecation: A Side Effect of Software Evolution</h3><p>Software doesn’t stand still. As projects grow, you might refactor code for better performance, outsource features to specialized packages, or align with industry standards. These improvements are essential for long-term viability, but they introduce deprecation as a necessary side effect. Here’s where the issues arise:</p><ol><li><strong>Balancing Evolution and Compatibility</strong>: Abruptly removing old functions or classes breaks user code, leading to frustration, bug reports, and lost trust. Yet, retaining everything forever results in bloated codebases, increased technical debt, and harder-to-maintain systems. You’re stuck in a trade-off: innovate or preserve the status quo?</li><li><strong>Warning Overload and User Annoyance</strong>: Using Python’s built-in warnings module is a start, but it’s rudimentary. Warnings can spam consoles endlessly, overwhelming users and diluting their impact. Customizing messages, limiting frequency, or routing them (e.g., to logs) requires boilerplate code that’s error-prone and time-consuming.</li><li><strong>Complex API Migrations</strong>: Not all deprecations are simple. Arguments might be renamed, dropped, or reordered in the new API. Manually handling remapping in every deprecated function duplicates effort and invites bugs. For classes or methods, the complexity multiplies, often requiring inheritance hacks or redundant implementations.</li><li><strong>Phased Rollouts and Version Management</strong>: Deprecations aren’t one-and-done. You need to specify timelines (e.g., deprecated in version X, removed in Y) and sometimes chain multiple levels for gradual changes. Conditional logic, like skipping based on dependencies or versions, adds another layer of manual work.</li><li><strong>Maintenance Burden on Developers</strong>: Without a dedicated tool, you’re reinventing deprecation logic across projects. This scatters focus from core features to housekeeping, especially in open-source, where user feedback loops are unpredictable.</li></ol><p>These pains aren’t just theoretical — they’ve plagued projects like scikit-learn or Django, where API stability is crucial. The result? Delayed releases, frustrated teams, and suboptimal user experiences. But what if there was a way to mitigate these issues without compromising on evolution or compatibility?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_k6zHujbf2UMi3QY6RWijw.png" /><figcaption>Generated image specificly for this blogpost.</figcaption></figure><blockquote>The full <a href="https://borda.github.io/pyDeprecate/stable/guide/use-cases.html">use-cases guide</a> covers real-world patterns across functions, methods, classes, Enums, dataclasses, and constants — including argument mapping and CI deadline enforcement.</blockquote><h3>Introducing pyDeprecate: A Simple Wrapper for Pain-Free Deprecations</h3><p>Enter <strong>pyDeprecate</strong>, a lightweight Python package designed to address these exact challenges. At its heart is a decorator-based wrapper that automates deprecation handling, allowing you to evolve your software while maintaining backward compatibility. Install it via pip install pyDeprecate, and you gain a tool that&#39;s Pythonic, extensible, and focused on developer efficiency.</p><p>Here’s how pyDeprecate delivers value by directly tackling the pains outlined above:</p><ul><li><strong>Effortless Forwarding and Compatibility</strong>: Wrap deprecated functions to automatically reroute calls to successors. Your old API remains functional during the transition, preventing breaks while gently guiding users to update. This preserves trust and buys time for adoption.</li><li><strong>Intelligent Warning Control</strong>: Limit warnings to a set number (e.g., 5 per function, or -1 to always warn) to avoid overload. Customize messages, streams (e.g., logging instead of stderr), and templates for clarity. No more console clutter — users get meaningful nudges without annoyance.</li><li><strong>Seamless Argument Remapping</strong>: Use built-in mapping to handle API mismatches. Rename or drop parameters on the fly, reducing manual intervention and bugs. The same args_mapping works whether you forward to another callable or remap arguments in place with TargetMode.ARGS_REMAP, and args_extra lets you inject new required arguments the old API never had.</li><li><strong>Flexible Phasing and Conditions</strong>: Define deprecation timelines and chain multiple wrappers for multi-stage rollouts. Add conditional skips (e.g., based on package versions) to adapt behavior dynamically, making complex migrations straightforward.</li><li><strong>Reduced Maintenance Overhead</strong>: By centralizing deprecation logic in a reusable decorator, pyDeprecate eliminates boilerplate. It’s lightweight (zero runtime dependencies, Python 3.9+ compatible) and tested rigorously, freeing you to focus on innovation rather than upkeep.</li></ul><p>In essence, pyDeprecate turns deprecation from a burden into a streamlined process. It’s not about adding features — it’s about removing friction, ensuring your software evolves sustainably while keeping users happy.</p><h4>Under the Hood: pyDeprecate’s Architecture</h4><p>pyDeprecate’s simplicity stems from its core @deprecated decorator, which wraps your code with smart logic:</p><ul><li><strong>Configuration</strong>: Pass parameters like target (a callable to forward to, TargetMode.ARGS_REMAP to remap arguments in place, or the default TargetMode.NOTIFY for a warn-only notice), versions (deprecated_in, remove_in), args_mapping, args_extra, num_warns, stream, skip_if, and update_docstring to inject the deprecation notice straight into your rendered API docs.</li><li><strong>Runtime Behavior</strong>: On call, it checks skip_if, issues capped warnings, remaps arguments, and forwards or proceeds accordingly. A thread-safe counter tracks warnings efficiently.</li><li><strong>Beyond the decorator</strong>: deprecated_class() wraps a class, Enum, or dataclass in a transparent proxy for a clean name change, and deprecated_instance() does the same for module-level constants and objects.</li><li><strong>Utilities</strong>: void quiets IDE warnings in empty bodies, assert_no_warnings keeps deprecation noise out of your tests, and the audit helpers (validate_deprecation_expiry, validate_deprecation_chains) enforce removal deadlines and catch accidental deprecation chains in CI.</li></ul><p>The core stays deliberately small and carries no runtime dependencies — the optional audit extra pulls in packaging only when you want CI deadline checks. Full details are in the <a href="https://github.com/Borda/pyDeprecate">GitHub repo</a>.</p><h3>Real-World Applications: A Few Patterns</h3><p>To illustrate, here are four scenarios where pyDeprecate shines. The <a href="https://borda.github.io/pyDeprecate/stable/guide/use-cases.html">use-cases guide</a> walks through the rest.</p><pre>pip install pyDeprecate</pre><h4>1. Function Forwarding for Quick Refactors</h4><p>Move or rename a function without breaking callers — the decorator forwards every call to the successor, so the old body becomes dead code.</p><pre>from deprecate import deprecated<br><br># NEW/FUTURE API - renamed to be explicit about what it computes<br>def compute_sum(a: int = 0, b: int = 3) -&gt; int:<br>    return a + b<br># DEPRECATED API - `calculate` was the original name before the rename<br>@deprecated(target=compute_sum, deprecated_in=&quot;0.1&quot;, remove_in=&quot;0.5&quot;)<br>def calculate(a: int, b: int = 5) -&gt; int:<br>    pass  # body is not needed - calls are forwarded to compute_sum<br>print(calculate(1, 2))  # 3, with a one-time FutureWarning</pre><h4>2. Argument Mapping for API Overhauls</h4><p>Bridge mismatched parameters, e.g., to scikit-learn — ideal for evolving Machine Learning (ML) APIs. Rename arguments, drop them (map to None), and route the notice wherever you like.</p><pre>import logging<br>from sklearn.metrics import accuracy_score<br>from deprecate import deprecated, void<br><br>@deprecated(<br>    target=accuracy_score,<br>    stream=logging.warning,<br>    num_warns=5,<br>    template_mgs=&quot;`%(source_name)s` was deprecated, use `%(target_path)s`&quot;,<br>    args_mapping={&quot;preds&quot;: &quot;y_pred&quot;, &quot;target&quot;: &quot;y_true&quot;, &quot;blabla&quot;: None},<br>)<br>def depr_accuracy(preds: list, target: list, blabla: float) -&gt; float:<br>    return void(preds, target, blabla)<br>print(depr_accuracy([1, 0, 1, 2], [0, 1, 1, 2], 1.23))  # 0.5</pre><h4>3. Self-Argument Remapping for Signature Changes</h4><p>When the function stays but its signature changes, use TargetMode.ARGS_REMAP to rename an argument in place. The notice fires only when a caller actually passes the old name, so anyone already migrated sees no noise.</p><pre>from deprecate import TargetMode, deprecated<br><br>@deprecated(<br>    target=TargetMode.ARGS_REMAP,<br>    args_mapping={&quot;coef&quot;: &quot;new_coef&quot;},<br>    deprecated_in=&quot;0.2&quot;,<br>    remove_in=&quot;0.4&quot;,<br>)<br>def any_pow(base: float, coef: float = 0, new_coef: float = 0) -&gt; float:<br>    &quot;&quot;&quot;`coef` is mapped to `new_coef`; the body only uses the new name.&quot;&quot;&quot;<br>    return base ** new_coef<br>print(any_pow(2, 3))  # 8</pre><h4>4. Class and Enum Deprecation</h4><p>Rename a class, Enum, or dataclass with deprecated_class() — a transparent proxy forwards all access to the replacement, no inheritance gymnastics required.</p><pre>from enum import Enum<br>from deprecate import deprecated_class<br><br># NEW/FUTURE API — renamed to be more descriptive<br>class ThemeColor(Enum):<br>    RED = 1<br>    BLUE = 2<br><br># DEPRECATED alias - the proxy forwards all access to ThemeColor<br>Color = deprecated_class(target=ThemeColor, deprecated_in=&quot;1.0&quot;, remove_in=&quot;2.0&quot;)(ThemeColor)<br>print(Color.RED is ThemeColor.RED)      # True<br>print(Color(1) is ThemeColor.RED)       # True<br>print(Color[&quot;RED&quot;] is ThemeColor.RED)   # True - one-time FutureWarning on first access</pre><p>When you instead need to forward a class’s construction to a successor, decorate its __init__ with @deprecated(target=SuccessorClass) so the notice fires at instantiation time.</p><h4>Enforcing the Timeline in CI</h4><p>A deprecation notice is only as good as the removal that eventually follows it. With the audit extra installed, validate_deprecation_expiry() raises in CI once a remove_in date has passed, so deprecated code cannot quietly outlive its deadline:</p><pre>pip install &#39;pyDeprecate[audit,cli]&#39;</pre><h3>Conclusion: Evolve Your Code with Confidence</h3><p>API deprecation is an unavoidable side effect of software growth, but it doesn’t have to be painful. By addressing compatibility tensions, warning chaos, and migration complexity, pyDeprecate lets you evolve your projects sustainably. Its wrapper-based approach delivers clear value: less effort, fewer bugs, and happier users — from a simple rename to proxy-wrapped Enums and multi-hop argument chains. The <a href="https://borda.github.io/pyDeprecate/stable/guide/use-cases.html">use-cases guide</a> covers the full set of patterns if you want to go deeper.</p><h3>Resources</h3><ul><li><a href="https://borda.github.io/pyDeprecate/"><strong>pyDeprecate documentation</strong></a> — full API reference, use-case patterns, CI audit tools</li><li><a href="https://borda.github.io/pyDeprecate/stable/getting-started.html"><strong>Getting started guide</strong></a> — installation + quick start</li><li><a href="https://borda.github.io/pyDeprecate/stable/guide/use-cases.html"><strong>Use cases with code</strong></a> — all deprecation patterns with working examples</li><li><a href="https://github.com/Borda/pyDeprecate">GitHub repository</a></li><li><a href="https://pypi.org/project/pyDeprecate/">PyPI</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1dbfd90e2b62" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codex/mastering-api-deprecation-in-python-the-pain-points-and-how-pydeprecate-can-help-1dbfd90e2b62">Mastering API Deprecation in Python: The Pain Points and How pyDeprecate Can Help</a> was originally published in <a href="https://medium.com/codex">CodeX</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Git sub-modules: How to automate updates via GitHub’s Pull Request]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/codex/git-sub-modules-how-to-automate-updates-via-githubs-pull-request-c822e546e499?source=rss-a89f75826628------2"><img src="https://cdn-images-1.medium.com/max/2600/1*NzTF_YYzjrMo271beCPT7g.jpeg" width="2688"></a></p><p class="medium-feed-snippet">There are several ways to define external dependencies in your project&#x200A;&#x2014;&#x200A;using a package manager if the dependency is installable or just&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/codex/git-sub-modules-how-to-automate-updates-via-githubs-pull-request-c822e546e499?source=rss-a89f75826628------2">Continue reading on CodeX »</a></p></div>]]></description>
            <link>https://medium.com/codex/git-sub-modules-how-to-automate-updates-via-githubs-pull-request-c822e546e499?source=rss-a89f75826628------2</link>
            <guid isPermaLink="false">https://medium.com/p/c822e546e499</guid>
            <category><![CDATA[dependencies]]></category>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[requirements]]></category>
            <category><![CDATA[gitsubmodule]]></category>
            <dc:creator><![CDATA[イルカ Borovec]]></dc:creator>
            <pubDate>Mon, 06 Nov 2023 12:47:01 GMT</pubDate>
            <atom:updated>2023-11-06T12:47:01.814Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Scalable Automation for Retry/Rerun Failed Checks in GitHub Actions]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/codex/scalable-automation-for-retry-rerun-failed-checks-in-github-actions-a732cce18b2a?source=rss-a89f75826628------2"><img src="https://cdn-images-1.medium.com/max/768/1*pLt1U7M1IB_xaHNKQaH54A.jpeg" width="768"></a></p><p class="medium-feed-snippet">A brief guide on how to automatically rerun (for N times) your failed checks/jobs with GitHub Actions on open Pull Requests.</p><p class="medium-feed-link"><a href="https://medium.com/codex/scalable-automation-for-retry-rerun-failed-checks-in-github-actions-a732cce18b2a?source=rss-a89f75826628------2">Continue reading on CodeX »</a></p></div>]]></description>
            <link>https://medium.com/codex/scalable-automation-for-retry-rerun-failed-checks-in-github-actions-a732cce18b2a?source=rss-a89f75826628------2</link>
            <guid isPermaLink="false">https://medium.com/p/a732cce18b2a</guid>
            <category><![CDATA[automation-tools]]></category>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[cicd-pipeline]]></category>
            <dc:creator><![CDATA[イルカ Borovec]]></dc:creator>
            <pubDate>Mon, 25 Sep 2023 19:06:41 GMT</pubDate>
            <atom:updated>2023-11-17T01:19:36.592Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Kaggle hacking: Validate a simple hypothesis against a hidden dataset]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/data-science/kaggle-hacking-validate-a-simple-hypothesis-against-a-hidden-dataset-4cf02bb16510?source=rss-a89f75826628------2"><img src="https://cdn-images-1.medium.com/max/2600/1*yZhz2Ik_q62TzZXwpiIYZQ.jpeg" width="3874"></a></p><p class="medium-feed-snippet">This post will share how to creatively debug your Kaggle submission, particularly the submission format, and how I ran a simple hypothesis&#x2026;</p><p class="medium-feed-link"><a href="https://medium.com/data-science/kaggle-hacking-validate-a-simple-hypothesis-against-a-hidden-dataset-4cf02bb16510?source=rss-a89f75826628------2">Continue reading on TDS Archive »</a></p></div>]]></description>
            <link>https://medium.com/data-science/kaggle-hacking-validate-a-simple-hypothesis-against-a-hidden-dataset-4cf02bb16510?source=rss-a89f75826628------2</link>
            <guid isPermaLink="false">https://medium.com/p/4cf02bb16510</guid>
            <category><![CDATA[notebook]]></category>
            <category><![CDATA[hacking]]></category>
            <category><![CDATA[hypothesis-testing]]></category>
            <category><![CDATA[kaggle-dataset]]></category>
            <dc:creator><![CDATA[イルカ Borovec]]></dc:creator>
            <pubDate>Thu, 05 May 2022 17:38:14 GMT</pubDate>
            <atom:updated>2022-05-05T17:38:14.761Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[How to Finetune Yolov5 in a Multi-GPU environment]]></title>
            <link>https://medium.com/data-science/how-to-finetune-yolov5-in-a-multi-gpu-environment-ada1935193d3?source=rss-a89f75826628------2</link>
            <guid isPermaLink="false">https://medium.com/p/ada1935193d3</guid>
            <category><![CDATA[yolov5]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[object-detection]]></category>
            <category><![CDATA[multi-gpu]]></category>
            <category><![CDATA[deep-learning]]></category>
            <dc:creator><![CDATA[イルカ Borovec]]></dc:creator>
            <pubDate>Tue, 15 Feb 2022 20:15:43 GMT</pubDate>
            <atom:updated>2022-02-26T00:08:47.909Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8WUGLZOUF_vfX48QGcgVNw.jpeg" /><figcaption>Illustration Photo by <a href="https://www.pexels.com/@babydov?utm_content=attributionCopyText&amp;utm_medium=referral&amp;utm_source=pexels">Ivan Babydov</a> from <a href="https://www.pexels.com/photo/magnifying-glass-placed-on-yellow-background-7788351/?utm_content=attributionCopyText&amp;utm_medium=referral&amp;utm_source=pexels">Pexels</a></figcaption></figure><h3>How to Fine-Tune YOLOv5 on Multiple GPUs</h3><h4>It is generally known that Deep Learning models tend to be sensitive to proper hyper-parameter selection. At the same time, when you search for the best configuration, you want to use maximal resources…</h4><p>Object detection is one of the advanced Computer Vision (CV) tasks that any ML/AI has not yet fully mastered. In short, the task is composed form localization and identification/classification of given objects in an image.</p><p><a href="https://machinelearningmastery.com/object-recognition-with-deep-learning/">A Gentle Introduction to Object Recognition With Deep Learning - Machine Learning Mastery</a></p><p>Object Detection is still quite a hot topic in the research space. Also, there is a relatively high demand for using such AI models in productions for many practical applications such as people detection in a scene or identification of items on a shop’s shelves. This naturally yields a larger collection of model architectures and even more implementations publically shared as open-source projects. One of them is <a href="https://github.com/ultralytics/yolov5">YOLO v5</a> which claims to have one of the best rations between performance (accuracy/precision) and inference time.</p><p>Besides training and inference, this project also offers running <a href="https://docs.ultralytics.com/tutorials/hyperparameter-evolution/">hyper-parameters search based on evolution algorithm tuning</a>. In a nutshell, the algorithm proceeds in generations, so it runs a few short training and chooses the best based on their performances. Then these best are blended with some minor random changes and trained again.</p><h3>Simple screen finetuning</h3><p>The simplest way to search for hyper-parameters is to run the training with an enabled evolution --evolve &lt;number&gt; argument. But this uses just a single GPU at most, so how about the remaining we have?</p><pre>python train.py \<br>    --weights yolov5s6.pt \<br>    --data /home/user/Data/gbr-yolov5-train-0.1/dataset.yaml \<br>    --hyp data/hyps/hyp.finetune.yaml \<br>    --epochs 10 \<br>    --batch-size 4 \<br>    --imgsz 3000 \<br>    --device 0 \<br>    --workers 8 \<br>    --single-cls \<br>    --optimizer AdamW \<br>    --evolve 60</pre><p>Eventually, we can run multiple training but how do we push them to collaborate? Luckily they can share a file with dumped training results from which the new population is drawn. Thanks to the randomness in the next generation, this can seem as exploring a much large population, as the <a href="https://github.com/ultralytics/yolov5/issues/918#issuecomment-1034818168">author states</a>.</p><p><a href="https://github.com/ultralytics/yolov5/issues/918">COCO Finetuning Evolution · Issue #918 · ultralytics/yolov5</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2kWvUWDMYZ8cWetm.jpg" /><figcaption>Illustration from <a href="https://colab.research.google.com/github/ultralytics/yolov5/blob/master/tutorial.ipynb">Ultralytics tutorial</a> with permission of <a href="https://github.com/glenn-jocher"><strong>Glenn Jocher</strong></a><strong>.</strong></figcaption></figure><h3>What are the options?</h3><p>Running multiple training processes while using a different GPU can be set by specifying it in the --device &lt;gpu-index&gt; argument. But how to maintain numerous processes and not lose them if you log out or accidentally brode your internet connection.</p><h4><a href="https://linuxhint.com/how_to_use_nohup_linux/">nohup</a></h4><p>It is the first and more trivial way to keep your process running.</p><pre>nohup python train.py ... &gt; training.log</pre><p>Unfortunately, there is <a href="https://serverfault.com/questions/367634/reattaching-a-process-started-with-nohup/367642#367642">no simple way to connect back</a> to the <a href="https://askubuntu.com/questions/57477/bring-nohup-job-to-foreground/57480#57480">once despatched process</a>, so it is mainly paired with redirecting stream to a file. Then you can constantly refresh the file, but it still does not always play nicely with progress bars that may be stretched over many lines.</p><h4><a href="https://linuxize.com/post/how-to-use-linux-screen/">screen</a></h4><p>This Unix application is convenient in many situations and here for spinning each process in its screen, and you can later traverse among all of them. Inside each screen, you have full access to control or kill the particular process.</p><pre>screen -S training<br>python train.py ...</pre><h4><a href="https://docs.docker.com/engine/">docker</a></h4><p>Another way is using docker containers with shared volume. Its advantage is that you can prepare customer docker images with a fixed environment that can eventually run anywhere, even on another server/cluster…</p><pre>docker build -t yolov5 .<br>docker run --detach --ipc=host --gpus all -v ~:$(pwd) yolov5 \<br>  python train.py ...<br>docker ps</pre><p>The commands above first build a docker image from the project folder. Later it spins a container and immediately detaches it with complete visibility to the GPUs and mapping the user home in the container to your local project folder. The last command is to list all running containers.</p><h3>Spin multiple collaborative dockers</h3><p>You would need to create each screen separately and start the particular training process within with screen. This extra work can be easily spoiled by choosing a wrong or already in use device.</p><p>An advantage of docker is that we can quickly write a loop to start as many containers as GPUs when spinning any docker container. The only limitation could be sufficient RAM which can also be limited on the docker side with --memory 20g argument. To properly utilize the shared dack of experiments, you need to fix project/name, set exist-ok and resume argument.</p><pre>for i in 1 2 3 4 5; do<br>  sudo docker run -d --ipc=host --gpus all \<br>    -v ~:/home/jirka \<br>    -v ~/gbr-yolov5/runs:/usr/src/app/runs \<br>    yolov5 \<br>  python /usr/src/app/train.py \<br>    --weights yolov5s6.pt \<br>    --data /home/jirka/gbr-yolov5-train-0.1-only_annotations/dataset.yaml \<br>    --hyp /usr/src/app/data/hyps/hyp.finetune.yaml \<br>    --epochs 10 \<br>    --batch-size 4 \<br>    --imgsz 3000 \<br>    --workers 8 \<br>    --device $i \<br>    --project gbr \<br>    --name search \<br>    --exist-ok \<br>    --resume \<br>    --evolve 60<br>done</pre><p>Later, to re-connect to the running container, for example, for monitoring the progress, you can <a href="https://www.baeldung.com/ops/docker-attach-detach-container#3-background-mode">enter the container with</a>:</p><pre>sudo docker ps<br>sudo docker attach --sig-proxy=false &lt;container-id&gt;</pre><p>Then use CTRL+c to detach back to your user terminal.</p><p>The last in case you do not want to let the training finish or you need to terminate all the running containers you call:</p><pre>sudo docker kill $(sudo docker ps -q)</pre><p><strong>Stay tuned and follow me to learn more!</strong></p><p><a href="https://www.kaggle.com/jirkaborovec/starfish-detection-flash-efficientdet">🐡Starfish detection: Flash⚡EfficientDet</a></p><h3>About the Author</h3><p><a href="https://medium.com/@jborovec"><strong>Jirka Borovec</strong></a> holds a Ph.D. in Computer Vision from CTU in Prague. He has been working in Machine Learning and Data Science for a few years in several IT startups and companies. He enjoys exploring interesting world problems, solving them with State-of-the-Art techniques, and developing open-source projects.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ada1935193d3" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/how-to-finetune-yolov5-in-a-multi-gpu-environment-ada1935193d3">How to Finetune Yolov5 in a Multi-GPU environment</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>