Scenka z życia: model działa tylko na laptopie
Data scientist dopina model klasyfikacji na swoim laptopie. Wszystko gra: metryki wysokie, API z Flaskiem śmiga lokalnie, demo na spotkaniu wypadło świetnie. Kiedy DevOps wrzuca ten sam kod i model na serwer produkcyjny, aplikacja wybucha błędami importu, a po „naprawieniu” zależności model nagle zaczyna przewidywać zupełnie inne wartości.
Źródło problemu prawie zawsze jest podobne: inne wersje Pythona, bibliotek lub systemu operacyjnego. Na laptopie jest Python 3.11, na serwerze 3.9. U Ciebie PyTorch 2.x z określoną wersją CUDA, na produkcji ktoś doinstalował TensorFlow, który ciągnie inną wersję sterowników. Numpy, który był kompilowany z innym BLAS-em, daje minimalnie inne wyniki obliczeń zmiennoprzecinkowych. To nie jest teoria – takie rozjazdy w uczeniu maszynowym pojawiają się zaskakująco często.
Tego typu kłopoty nie dotyczą wyłącznie wielkich zespołów i rozbudowanych klastrów GPU. Wystarczy mały projekt, jeden serwer i jeden laptop. Im bardziej „na skróty” kopiowane jest środowisko – ręczne pip install, dopisywanie bibliotek „bo krzyczy, że brakuje” – tym więcej loterii. Z drugiej strony, zaplanowany proces oparty na Condzie i Dockerze pozwala potraktować środowisko jako część artefaktu modelu, a nie luźną notatkę w README.
Różnica między chaosem a stabilnością jest zasadnicza. Chaos to: „u mnie działa, u was nie, pewnie coś z serwerem”. Stabilność to: zdefiniowane środowisko Conda do developmentu, Dockerfile, który hermetyzuje wszystko od systemu po kod, oraz spójny sposób przenoszenia modeli między lokalnym notebookiem, serwerem firmowym a chmurą. Wtedy nawet po kilku miesiącach da się odtworzyć trening i inference, a błędy z kategorii „nie wiadomo czemu” praktycznie znikają.
Co właściwie znaczy „przeniesienie modelu między środowiskami”
Trzy elementy: kod, artefakt modelu i środowisko wykonawcze
Przenoszenie modelu ML większości osób kojarzy się wyłącznie z plikiem modelu: .pkl, .pt, .onnx czy całym katalogiem z wagami. Tymczasem to tylko jeden z trzech elementów.
Pełny obraz przenoszenia modelu obejmuje:
- Kod – skrypty treningowe, kod inference (np. serwer REST), preprocessing, pipeline danych.
- Artefakt modelu – zapisane wagi, architektura, parametry (czasem w jednym pliku, czasem w kilku).
- Środowisko wykonawcze – Python i jego biblioteki, biblioteki natywne (C/C++), system operacyjny, sterowniki GPU/CPU.
Gdy kopiujesz tylko kod i model, a ignorujesz środowisko, oddajesz się w ręce przypadku. Drobne różnice w numpy, Pandas, PyTorch, a nawet w wersji kompilatora mogą spowodować, że predykcje się zmienią lub aplikacja w ogóle nie wystartuje.
Typowe ścieżki migracji modeli ML
Praktyczne ścieżki migracji, gdzie konflikt zależności wychodzi na jaw, to przede wszystkim:
- Lokalny notebook → serwer firmowy – klasyczny scenariusz: praca na Windows + Anaconda, deployment na Linuxie z systemowym Pythonem.
- On-prem → chmura – ten sam model migruje z fizycznej maszyny w serwerowni do AWS/GCP/Azure, często z inną dystrybucją Linuksa.
- Trening → endpoint inference – środowisko treningowe (bogate, ciężkie, z pełnym toolsetem) a środowisko służące tylko do serwowania predykcji (zwykle lekkie, okrojone).
- Data scientist → zespół produkcyjny – osoba od modeli używa Condy, zespół DevOps woli czystego Dockera i systemowe pakiety.
Na każdej z tych ścieżek można postąpić w sposób „na skróty” lub zbudować powtarzalny proces. Różnica kosztuje później godziny debugowania i nerwów całego zespołu.
„Da się odpalić” kontra „da się odtworzyć krok po kroku”
Model „da się odpalić”, gdy po serii ręcznych poprawek, doinstalowaniu kilku bibliotek i zmianie czegoś w kodzie w końcu zadziała. Kłopot polega na tym, że po kilku tygodniach nikt już nie pamięta, jakie dokładnie kroki zostały wykonane. Jeśli serwer trzeba przeinstalować albo odtworzyć środowisko dla innego klienta, zabawa zaczyna się od zera.
Model „da się odtworzyć krok po kroku”, gdy istnieje opis środowiska (np. environment.yml Condy i Dockerfile), z którego można zbudować identyczną konfigurację. Bez ręcznego doinstalowywania, bez „magicznych” ustawień. To właśnie jest reprodukowalność środowiska ML – możesz wrócić do danej wersji modelu, danych i środowiska i uzyskać te same wyniki.
Poziomy przenoszenia: od Pythona do pełnej infrastruktury
Da się wyróżnić kilka poziomów „seriości” w przenoszeniu modeli:
- Tylko Python – eksport requirements.txt, instalacja na docelowej maszynie, bez kontroli systemu i bibliotek natywnych.
- Conda lub pipenv/poetry – lepsza kontrola pakietów, czasem z pinowaniem wersji, ale wciąż bez pełnej izolacji systemowej.
- Docker – hermetyzacja całego systemu: OS, Python, biblioteki, kod i model w jednym obrazie.
- Pełna infrastruktura as code – Docker + Kubernetes, Terraform/CloudFormation, MLflow/Weights&Biases do śledzenia eksperymentów i środowisk. Tu model jest jednym z elementów większej układanki.
Dla większości zespołów data science w małych i średnich firmach wystarczy połączenie: Conda dla developmentu + Docker dla środowisk testowych i produkcyjnych. Wszystko ponad to jest rozszerzeniem tej samej idei: środowisko jest częścią artefaktu, a nie dodatkiem „na słowo honoru”.
Środowisko jako część artefaktu modelu
Dopóki środowisko nie jest wersjonowane razem z modelem, migracja jest loterią. Plik model.pkl to za mało, by mówić o powtarzalnym procesie. Trzeba do niego dołączyć:
- opis pakietów i ich wersji (Conda, pip, często oba),
- informację o wersji Pythona,
- konkretny obraz bazowy Dockera (z nazwą i tagiem),
- ewentualnie: wersję CUDA, sterowników GPU, bibliotek systemowych.
Dopiero wtedy model jako artefakt ma naprawdę trzy wymiary: kod, dane modelu i środowisko. Taki pakiet da się bezpiecznie przenieść z laptopa na serwer, a potem do chmury, bez uciążliwego odkrywania na nowo, co właściwie jest potrzebne, by go uruchomić.

Fundamenty środowisk dla ML: Python, system i biblioteki natywne
Warstwy: system operacyjny, biblioteki C/C++, Python i menedżery pakietów
Z punktu widzenia przenoszenia modeli warto spojrzeć na środowisko ML warstwowo. Od dołu do góry wygląda to mniej więcej tak:
- System operacyjny – dystrybucja Linuksa (Ubuntu, Debian, CentOS, Alpine), Windows, macOS. W ML najczęściej Linux, bo najlepiej współpracuje z GPU i bibliotekami natywnymi.
- glibc i inne biblioteki systemowe – często niewidoczne na pierwszy rzut oka, ale kluczowe. Prekompilowane pakiety (np. numpy, PyTorch) są powiązane z określonymi wersjami tych bibliotek.
- Biblioteki C/C++ – BLAS (OpenBLAS, MKL), LAPACK, OpenMP, biblioteki do obsługi GPU (CUDA, cuDNN, NCCL), kompilatory.
- Python – konkretna wersja interpretera: 3.8, 3.10, 3.11 itd. Z różnicami w ABI, obsługiwanych funkcjach i kompatybilności z pakietami.
- Pakiety Python (pip/conda) – numpy, pandas, scikit-learn, PyTorch, TensorFlow, xgboost, biblioteki webowe (FastAPI, Flask), logger, itp.
Docker i Conda działają na różnych poziomach tej piramidy. Docker pakietuje wszystko od systemu w górę. Conda izoluje głównie Pythona i pakiety (oraz część natywnych bibliotek), ale bazuje na systemie hosta lub obrazu bazowego, jeżeli uruchamiana jest wewnątrz Dockera.
Dlaczego uczenie maszynowe jest szczególnie wrażliwe na zależności
Klasyczne aplikacje webowe często przechodzą przez zmiany wersji bibliotek bez spektakularnych efektów. W ML zmiana wersji nawet jednego pakietu potrafi wprowadzić subtelne zmiany numeryczne lub zupełnie różne zachowanie algorytmu. Kilka punktów, które szczególnie lubią psuć reprodukowalność:
- numpy, scipy, pandas – różne implementacje operacji, inne optymalizacje, zmiany w domyślnych typach danych.
- PyTorch / TensorFlow – powiązane ściśle z wersjami CUDA i cuDNN; zmiana wersji może wpływać na dokładność liczb zmiennoprzecinkowych, kolejność operacji i deterministyczność.
- BLAS, OpenMP, MKL – inne biblioteki numeryczne pod spodem mogą prowadzić do mikroróżnic w obliczeniach, które w skrajnych przypadkach kumulują się i zmieniają wynik.
- GPU vs CPU – modele trenowane na GPU mogą zachowywać się nieco inaczej niż inferowane na CPU, zwłaszcza w obszarach podatnych na niestabilność numeryczną.
Do tego dochodzi jeszcze losowość: seed, deterministyczne flagi w PyTorch/TensorFlow, liczba wątków, ustawienia bibliotek. Jeżeli już na poziomie wersji pakietów jest chaos, to odtworzenie dokładnych warunków treningu staje się praktycznie niemożliwe.
Co faktycznie „niesie” Docker, a co Conda
Docker jest mechanizmem konteneryzacji. W praktyce oznacza to, że:
- tworzysz obraz – gotowy snapshot systemu operacyjnego z zainstalowanym Pythonem, bibliotekami, kodem i modelem,
- uruchamiasz z niego kontener – lekką izolowaną instancję, która udaje osobną maszynę.
Docker niesie więc całe środowisko: dystrybucję Linuksa, glibc, wszystkie natywne biblioteki, wersję Pythona, pakiety, a nawet zależności systemowe (curl, git itp.). To pełna hermetyzacja – byleby host potrafił uruchamiać kontenery Dockera.
Conda jest natomiast menedżerem środowisk i pakietów. Tworzy osobne katalogi, gdzie umieszcza Pythona i biblioteki (w tym część natywnych). W granicach jednego systemu hosta można mieć kilkanaście izolowanych środowisk Conda, każde z własną wersją Pythona i zależności. Conda nie izoluje jednak w pełni warstwy systemowej – dystrybucja Linuksa, glibc czy sterowniki GPU pochodzą z hosta lub obrazu bazowego.
Najczęstsze rozjazdy między środowiskami
Do najpopularniejszych źródeł niespodzianek należą:
- Różne dystrybucje Linuksa – lokalnie Ubuntu, na serwerze CentOS; inne wersje bibliotek systemowych, inny glibc.
- Inna wersja Pythona – część bibliotek jest kompatybilna tylko z konkretnymi wersjami (np. niektóre pakiety ML długo nie wspierają najnowszego Pythona).
- GPU/CPU – na laptopie bez GPU użyto wersji CPU-only, na serwerze wersji GPU; albo odwrotnie. To samo dotyczy wersji CUDA i sterowników.
- Mieszanie menedżerów pakietów – instalowanie części zależności conda, części pip, a czasem jeszcze przez systemowego apt, co potrafi tworzyć trudne do debugowania konflikty.
Sama kontrola nad requirements.txt obejmuje tylko fragment piramidy: pakiety pip dla konkretnego Pythona. Bez ustalenia wersji Pythona, dystrybucji systemu, CUDA i bibliotek natywnych trudno mówić o reprodukowalności na poważnie.
Conda – budowanie powtarzalnego środowiska developerskiego
Tworzenie środowiska Conda z precyzyjnymi wersjami
Conda świetnie sprawdza się jako narzędzie do stworzenia stabilnego środowiska developerskiego dla projektu ML. Podstawowy krok to stworzenie nowego środowiska z konkretną wersją Pythona:
conda create -n moj_projekt python=3.10
conda activate moj_projekt
Zamiast instalować wszystko „na żywioł”, lepiej od razu przemyśleć zestaw zależności: numpy, pandas, scikit-learn, konkretny framework (PyTorch / TensorFlow), biblioteki do logowania, narzędzia pomocnicze. Im mniej pakietów, tym łatwiej później utrzymać środowisko i debugować konflikty.
Dobrym pomysłem jest też świadome używanie kanałów. Zamiast mieszać domyślny kanał defaults z conda-forge, lepiej konsekwentnie używać jednego, zwykle conda-forge, bo ma bogatszy zestaw pakietów oraz spójną politykę budowania.
conda create -n moj_projekt python=3.10 -c conda-forge
environment.yml – opis środowiska jako kod
Lockfile zamiast „zobaczymy, co się zainstaluje”
Ktoś z zespołu robi conda env export > environment.yml, a po kilku tygodniach druga osoba próbuje odtworzyć środowisko i dostaje inny zestaw wersji. Z zewnątrz wszystko wygląda podobnie, ale model nagle ma inne metryki. Różnica? Pierwsze środowisko powstało krok po kroku, drugie – z pliku z luźnymi zależnościami.
Conda umożliwia dwa typy opisów środowiska:
- logiczny – z nazwami pakietów i przybliżonymi wersjami (np.
pytorch>=2.0), - konkretny (lockfile) – z dokładnymi wersjami i kanałami (często też z informacjami o platformie).
Do pracy bieżącej wystarcza często opis logiczny. Do przenoszenia modelu i reprodukcji – lepszy jest wariant zablokowany. Klasyczne conda env export produkuje jednak sporo „szumu” (pakiety techniczne, zależności, które akurat się zainstalowały). Dlatego praktyczniejszy proces wygląda często tak:
- Tworzysz środowisko „ręcznie” (lub z krótkiego environment.yml z najważniejszymi pakietami).
- Testujesz, trenujesz model, upewniasz się, że wszystko działa.
- Tworzysz dokładny snapshot środowiska, np. za pomocą:
conda env export --from-history > environment_base.yml– tylko pakiety jawnie instalowane,- dodatkowo narzędzia typu conda-lock, które generują osobny plik z przypiętymi wersjami dla danej platformy.
Te dwa pliki pełnią inne role: environment_base.yml jest deklaracją intencji („czego używamy w projekcie”), a lockfile – dokładnym przepisem dla CI/CD i produkcji. Model powinien być zawsze powiązany z konkretnym lockfilem, a nie tylko z luźnym opisem środowiska.
Jak nie zabić się mieszając conda i pip
Typowa scena: instalacja większości bibliotek przez conda, ale jeden niszowy pakiet istnieje tylko w pip. Szybkie pip install „na środku” procesu i nagle coś zaczyna się sypać, bo pip nadpisał bibliotekę z conda inną wersją.
Da się to zrobić bezpieczniej, ale wymaga to kilku zasad:
- Najpierw conda, potem pip – najpierw instalujesz wszystko, co się da z conda (w jednym kroku), a dopiero później brakujące elementy z pip.
- Unikasz dublowania pakietów – jeżeli coś zainstalowałeś conda, nie instaluj tego samego z pip „nowszej wersji”. To proszenie się o konflikty ABI.
- Dokumentujesz pakiety pip osobno – np. sekcja
pip:w environment.yml, żeby było jasne, która część jest poza kontrolą conda.
Przykładowy fragment environment.yml może wyglądać tak:
name: moj_projekt
channels:
- conda-forge
dependencies:
- python=3.10
- numpy=1.26
- pandas=2.1
- scikit-learn=1.3
- pip
- pip:
- some-rare-package==0.4.2
Jeżeli projekt zaczyna mieć więcej pakietów pip niż conda, to sygnał, że to środowisko może być trudne do utrzymania i lepiej rozważyć inne podejście (np. „czyste” pip + venv, albo przejście na dockerowe obrazy zoptymalizowane pod pip).
Wersjonowanie plików environment i powiązanie z modelem
Model przechodzi kilka iteracji: model_v3.pkl, model_v4.pkl, model_best.pkl. Po kilku miesiącach nikt nie pamięta, na jakim dokładnie środowisku powstał ten „best”. Replikacja wyników jest wtedy grą w zgadywanie.
Lepszy wzorzec to traktowanie pliku środowiskowego jak części wersji modelu, np.:
models/2023-08-15-churn-v1/model.pklmodels/2023-08-15-churn-v1/environment.lock.yml
Do tego krótki metadany plik (JSON/YAML) z informacją, jakiego obrazu Dockera użyto lub jak odtworzyć środowisko. Dzięki temu za pół roku wystarczy pobrać trzy pliki, odpalić conda env create -f environment.lock.yml albo uruchomić odpowiedni kontener, by mieć takie same warunki jak w momencie trenowania.
Diagnostyka rozjazdów środowiskowych na poziomie Conda
Gdy model inaczej zachowuje się na serwerze niż u autora, kusi, żeby od razu szukać błędów w kodzie. Tymczasem często problemem jest minimalna różnica w wersjach pakietów. Kilka prostych kroków diagnostycznych potrafi zaoszczędzić godziny:
- Zrzut listy pakietów –
conda list --explicit > spec.txtna obu maszynach i porównanie plików (diff). - Sprawdzenie wersji Pythona i platformy –
python -V,conda info,uname -a. - Weryfikacja CUDA / sterowników –
nvidia-smi,conda list | grep cudatoolkit,torch.version.cudaw Pythonie.
Jeżeli różnice są niewielkie (np. numpy 1.23 vs 1.24), łatwo je spiąć lockfilem i odtworzyć jedno z nich. Jeżeli jednak rozjazd dotyczy całej piramidy (inna dystrybucja, inna CUDA, inny Python), to znak, że pora sięgnąć po Dockera jako główny nośnik środowiska.
Docker – hermetyzacja środowiska od systemu po kod
Od prostego Dockerfile do stabilnego obrazu produkcyjnego
Na początku zwykle powstaje szybki Dockerfile „do przetestowania”: bazowy obraz, instalacja kilku pakietów, kopiowanie kodu. Obraz działa, ale nikt nie wie, dlaczego waży kilka gigabajtów ani co dokładnie jest w środku. Przy pierwszym większym update’cie zaczyna się walka z zależnościami.
Solidny obraz dla modelu ML ma kilka charakterystycznych cech:
- konkretny obraz bazowy – np.
python:3.10-slimalbopytorch/pytorch:2.1.0-cuda11.8-cudnn8-runtimez przypiętym tagiem, a nielatest, - jedno źródło prawdy dla zależności – requirements.txt, environment.yml albo pliki generowane przez Poetry/pip-tools,
- warstwy zorganizowane według częstości zmian – system i biblioteki na dole, kod i model na górze, żeby przebudowywać jak najmniej.
Przykładowy Dockerfile dla prostego modelu CPU-only może wyglądać tak:
FROM python:3.10-slim
# Uzupełnienie minimalnych zależności systemowych
RUN apt-get update && apt-get install -y --no-install-recommends
build-essential
&& rm -rf /var/lib/apt/lists/*
# Ustawienia środowiska
ENV PYTHONDONTWRITEBYTECODE=1
PYTHONUNBUFFERED=1
WORKDIR /app
# Najpierw zależności, potem kod (lepsze cache)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "serve.py"]
Niewiele tu magii, ale kluczowe są detale: brak latest, minimalny zestaw pakietów systemowych, osobna warstwa na zależności. W praktyce obraz staje się przewidywalny, a przebudowa po zmianie modelu trwa sekundy, nie minuty.
Modele GPU i obrazy z CUDA – gdzie można się potknąć
Przeniesienie modelu GPU w Dockera często wygląda tak: ktoś bierze randomowy obraz z Docker Huba, dodaje kod, odpala – i dostaje błąd o braku zgodności wersji CUDA. Raz działa, raz nie, w zależności od tego, na którym serwerze stoi kontener.
Bezpieczniejszy sposób to oprzeć się na oficjalnych obrazach z już „spiętą” wersją CUDA i frameworka, np. dla PyTorch:
FROM pytorch/pytorch:2.1.0-cuda11.8-cudnn8-runtime
RUN apt-get update && apt-get install -y --no-install-recommends
git
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY environment.yml .
RUN conda env update -n base -f environment.yml
COPY . .
ENV TORCH_CUDNN_V8_API_ENABLED=1
CMD ["python", "serve_gpu.py"]
Kilka praktycznych zasad przy obrazach GPU:
- sterownik GPU (na hoście) musi być kompatybilny z wersją CUDA „oczekiwaną” przez obraz,
- nie miesza się wielu wersji CUDA w jednym kontenerze, chyba że jest do tego bardzo konkretny powód,
- jeżeli model wymaga deterministyczności, sprawdza się dokumentację frameworka pod kątem flag deterministycznych i wsparcia w danej kombinacji wersji CUDA/cuDNN.
Im mocniej środowisko GPU jest oparte na oficjalnych obrazach frameworków, tym mniej czasu spędza się na debugowaniu różnic między serwerami.
Łączenie Docker i Conda – kiedy ma sens, a kiedy przeszkadza
Częsty pomysł: „wrzućmy Condę do Dockera, będzie łatwiej, bo dev używa Condy”. To czasem działa, ale bywa też źródłem nadmiarowej złożoności: masz wtedy menedżera środowisk w środku już izolowanego środowiska.
Scenariusze, gdzie połączenie ma sens:
- duży projekt badawczy, który ma różne warianty środowisk (sub-eksperymenty) i potrzeba dynamicznego przełączania się między nimi wewnątrz jednego obrazu,
- zespół ma rozbudowane environment.yml i lockfile, a Docker służy „tylko” jako cienka warstwa systemowa (np.
ubuntu:22.04+miniconda+ środowisko z pliku).
Jeżeli jednak celem jest wyłącznie stabilny serwis inferencyjny, prostsze bywa zainstalowanie wszystkiego bezpośrednio pipem w obrazie python:slim lub gotowym obrazie frameworka. Każda dodatkowa warstwa (Conda, micromamba, kolejne katalogi środowisk) to potencjalne źródło pomyłek przy konfiguracji ścieżek i zmiennych środowiskowych.
Rozbijanie środowiska na obrazy etapowe
W miarę jak model obrasta zależnościami, obraz Dockera puchnie. W logach CI zaczyna się maraton pobierania gigabajtów bibliotek, a developerzy przestają aktualizować środowisko, bo „build trwa za długo”. Rozwiązaniem są często obrazy etapowe (multi-stage builds) i podział odpowiedzialności.
Przykładowy wzorzec:
- Obraz „base” – system + Python + główne biblioteki ML (numpy, pandas, PyTorch).
- Obraz „runtime” – dziedziczy po base, dodaje tylko kod aplikacji i model.
- Ewentualnie obraz „training” – osobny, cięższy obraz z narzędziami do trenowania (kompilatory, notebooki, debugery), który nie trafia na produkcję.
Taki podział daje dwie korzyści: buildy produkcyjne zużywają cache z warstw „base”, a obrazy serwisowe pozostają lżejsze, bo nie zawierają narzędzi używanych tylko przy treningu. Jednocześnie wciąż da się jasno wskazać, na jakim obrazie bazowym powstał konkretny model.
Debugowanie różnic: kontener jako „tymczasowy laptop”
Gdy produkcja zachowuje się inaczej niż środowisko deweloperskie, pokusa jest taka: „ściągnijmy dane z produkcji na lokalny laptop i zobaczmy”. W praktyce często prościej i bezpieczniej jest wejść do tego samego obrazu Dockera, który działa na serwerze, i lokalnie odtworzyć scenariusz.
Typowy workflow wygląda tak:
# uruchomienie kontenera z interaktywną powłoką
docker run -it --rm
-v $(pwd):/workspace
--gpus all
my-registry/model-serving:2023-08-15
bash
# wewnątrz kontenera
cd /workspace
python debug_script.py
Dzięki temu debug odbywa się w środowisku 1:1 z produkcją: te same biblioteki systemowe, ta sama wersja Pythona, ten sam framework. Jeżeli wynik jest inny niż na laptopie, wiadomo, że problem leży w różnicy środowisk; jeżeli taki sam – można skupić się na innych elementach (np. danych wejściowych).
Tagowanie obrazów i śledzenie, który model gdzie działa
W rejestrze kontenerów szybko robi się tłoczno: model:latest, model:v2, model:test. Po kilku miesiącach nikt nie pamięta, co gdzie jest i jaki dokładnie model siedzi w środku obrazu „v2”. Przy awarii ciężko odtworzyć, jaka kombinacja kodu i środowiska faktycznie była na produkcji.
Sprawę upraszcza konsekwentne tagowanie, które łączy:
- wersję modelu (np. hash eksperymentu z MLflow lub innego narzędzia),
- wersję kodu (hash Gita lub tag release’u),
- datę lub numer builda CI.
Konwencje wersjonowania: model, kod, środowisko
Wyobraź sobie alert z produkcji: „Model fraud-detection daje dziwne wyniki dla kart w EUR”. Zalogowany jest tylko tag obrazu Dockera: fraud-detector:2024-02-17. Nikt nie wie, jaka to wersja modelu, jakie zależności i czy to na pewno ten sam eksperyment, który kilka tygodni wcześniej dostał najlepszy wynik w notebooku.
Żeby nie szukać igły w stogu siana, zestaw: model – kod – środowisko trzeba ze sobą związać w przejrzysty sposób. W praktyce oznacza to spójne nazewnictwo tagów obrazów Dockera i adnotacje wewnątrz obrazu.
Przydatny schemat tagu obrazu może wyglądać tak:
my-registry/fraud-detector:
model-6f3a92f_
code-a1c2d3e_
2024-02-17-01
Taki tag jest długi, ale za to jednoznaczny. Z poziomu samego obrazu łatwo potem wyciągnąć te informacje:
- zawartość pliku
/app/VERSIONz wersją modelu i gita, - zmienne środowiskowe, np.
MODEL_VERSION,CODE_SHA, - etykiety Dockera (labels), które CI dopisuje w trakcie budowy obrazu.
Przykład fragmentu Dockerfile’a z takimi adnotacjami:
ARG MODEL_VERSION
ARG CODE_SHA
LABEL model.version=$MODEL_VERSION
code.sha=$CODE_SHA
ENV MODEL_VERSION=$MODEL_VERSION
CODE_SHA=$CODE_SHA
Gdy po kilku miesiącach trzeba odtworzyć, „co dokładnie wtedy działało”, wystarczy pobrać obraz z rejestru, podejrzeć etykiety i zmienne, a potem w narzędziach typu MLflow czy DVC odnaleźć konkretny eksperyment i dane uczące.
Reprodukowalne buildy: od Dockerfile do powtarzalnego obrazu
Git w repo, Conda w dev i Docker w produkcji nie gwarantują, że za pół roku zbudujesz ten sam obraz. Wystarczy, że upstreamowy obraz bazowy zmieni zawartość pod tym samym tagiem albo zewnętrzne repo pakietów zniknie.
Żeby zminimalizować to ryzyko, przy budowie obrazu dobrze jest dopiąć kilka śrub:
- pinowanie obrazów bazowych nie tylko tagiem, ale jeśli się da – także digestem (hash warstwy), np.
python:3.10-slim@sha256:..., - pełne lockfile – generowane przez
conda-lock,pip-tools, Poetry, zamiast „luźnych” wersji>=w requirements, - zerwanie z
apt-get upgradei instalowanie tylko konkretnych pakietów systemowych, bez bezrefleksyjnych update’ów.
Często pomaga też rozdzielenie „build-time” i „run-time” zależności. W multi-stage buildzie pierwszy etap zawiera kompilatory i nagłówki, drugi – tylko to, co jest potrzebne do uruchomienia modelu. Dzięki temu finalny obraz jest mniejszy, a ryzyko przypadkowych zmian spada.
Dla projektów, w których stabilność jest krytyczna (modele finansowe, medyczne), niektórzy idą krok dalej i archiwizują gotowe obrazy w osobnym registry tylko do odtwarzania historycznych wersji. Z punktu widzenia audytu czy analizy incydentu daje to ogromny komfort.
Przenoszenie modelu z Condy do Dockera krok po kroku
Częsty schemat jest taki: całe R&D jedzie na Condzie, a gdy pojawia się potrzeba produkcji, ktoś „na szybko” tłumaczy environment.yml na requirements.txt. Potem następuje seria niespodzianek: brakuje biblioteki systemowej, niby ta sama wersja Pythona, ale inna kompilacja, a model czasem rzuca segmentation fault.
Bezpieczniejsza ścieżka to potraktowanie istniejącego środowiska Conda jako źródła prawdy i świadome „spłaszczenie” go do obrazu Dockera. Jedno z praktycznych podejść wygląda tak:
- W dev wyczyść środowisko: usuń nieużywane pakiety, upewnij się, że
environment.ymlnie zawiera „śmieci” sprzed miesięcy. - Wyeksportuj środowisko z wyraźnym pinowaniem:
conda env export --from-history(tylko zainstalowane świadomie pakiety), a potem wygeneruj lockfile przy pomocyconda-lock. - W Dockerze użyj
minicondalubmambyi w pierwszej warstwie załaduj właśnie ten lockfile.
Przykładowy szkic Dockerfile’a oparty na Condzie:
FROM mambaorg/micromamba:1.5.0
WORKDIR /app
COPY environment.lock.yml /tmp/environment.lock.yml
RUN micromamba create -y -n model-env -f /tmp/environment.lock.yml &&
micromamba clean --all --yes
COPY . .
SHELL ["micromamba", "run", "-n", "model-env", "/bin/bash", "-c"]
CMD ["micromamba", "run", "-n", "model-env", "python", "serve.py"]
W takim podejściu code-path w dev i w obrazie produkcyjnym jest bardzo podobny. Różnice dotyczą głównie systemu operacyjnego, a nie wersji Pythona czy głównych bibliotek ML.
Przenoszenie modelu między klastrami: lokalny Docker vs Kubernetes
Na laptopie wszystko chodzi w docker run .... W firmowym klastrze Kubernetes pojawia się YAML, limity zasobów i tajemnicze „pod nie może wystartować, CrashLoopBackOff”. Jednocześnie to ten sam obraz, ta sama wersja modelu, ten sam serwer inferencyjny.
Różnica między „lokalnym Dockerem” a klastrem wynika głównie z otoczenia: sposób montowania wolumenów, dostęp do GPU, ograniczenia pamięci, sieć. Zamiast szukać błędu w modelu, częściej opłaca się przejrzeć, jak obraz jest uruchamiany.
Przy przenoszeniu modelu z jednego klastra do drugiego pomagają powtarzalne wzorce manifestów, na przykład:
- Deployment z jasno zadeklarowanym
resources.requestsiresources.limits, - użycie
nodeSelectorlubtaints/tolerationsdla GPU, zamiast „na ślepo” dopisywanego--gpus all, - ConfigMap lub Secret na konfigurację modelu (feature flagi, endpointy do feature store’ów), zamiast wbudowywania wszystkiego do obrazu.
Delikatnym, ale ważnym punktem są lokalne ścieżki do modeli: to, co w Dockerze było zwykłym ./models/model.bin, w Kubernetesie powinno zwykle trafić do wolumenu (PVC, hostPath lub obiektowego storage z cachem lokalnym). W przeciwnym razie każda replika poda będzie kopiować model z sieci przy restarcie, a różnice w czasie startu zaczną mieć wpływ na SLA.
„Cichy dryf” środowiska: jak go wykrywać
Czasem model nie psuje się z dnia na dzień, tylko powoli: drobna zmiana obrazu bazowego, nowa łatka bezpieczeństwa w systemie, update mikroserwisu, który przygotowuje featury. Na końcu ktoś widzi, że statystyki się rozjechały, ale nie umie powiedzieć, kiedy dokładnie.
Do kontroli takiego „dryfu” środowiska przydają się proste, ale konsekwentne mechanizmy:
- logowanie metadanych środowiska przy starcie procesu inferencyjnego (np.
torch.__version__,np.__version__, wersja CUDA), - utrzymywanie małego zestawu testów kontraktowych, które uruchamia się na każdym buildzie obrazu (np. kilka znanych wejść i oczekiwane wyjścia z modelu w ramach akceptowalnej tolerancji),
- okresowe porównywanie środowisk pomiędzy kluczowymi punktami (laptop – staging – produkcja) narzędziem typu
pip freeze/conda list+ diff.
Prosty skrypt startowy, który wypisuje „manifest środowiska” i zapisuje go w logach, potrafi uratować godziny śledztwa. Gdy pojawia się dziwny błąd, można w logach sprawdzić, jaka dokładnie kombinacja wersji towarzyszyła danemu requestowi.
Specjalne przypadki: modele zależne od zewnętrznych binarek
Pojawiają się też mniej typowe sytuacje: model korzysta z zewnętrznego silnika wyszukiwania, transkodera wideo, narzędzia CLI do tokenizacji. Na laptopie wszystko jest zainstalowane „od zawsze”, w kontenerze – zapomniano o jednej binarce i całość wywraca się w najmniej odpowiednim momencie.
W takich projektach lista zależności nie kończy się na requirements.txt. Potrzebny jest katalog „zewnętrznych” komponentów:
- problemów z architekturą (binarka zbudowana na macOS nie zadziała w linuksowym kontenerze),
- licencji (część narzędzi nie może być swobodnie pakowana do obrazu),
- ścieżek i uprawnień (czy model może wykonywać zewnętrzny proces w środowisku produkcyjnym?).
Bezpiecznym nawykiem jest stworzenie w repo pliku typu EXTERNAL_DEPENDENCIES.md lub sekcji w README, gdzie wypisane są wszystkie zaciski: wymagane pakiety systemowe, potrzebne binarki, powiązane usługi (np. lokalny serwer faiss, Redis, wektorowa baza danych). Dockerfile staje się wtedy odtworzeniem tej listy, a nie zgadywanką.
Strategie migracji „żywych” modeli: blue-green, shadow i canary
Inna pułapka pojawia się przy wymianie modelu działającego już w produkcji. Technicznie: nowy obraz Dockera jest gotowy, manifesty poprawne, zależności też. Biznesowo: jeśli coś pójdzie źle, po kilku minutach telefony od klientów.
Do przenoszenia modeli między środowiskami (stara produkcja → nowa produkcja) stosuje się kilka wzorców, w których Docker i powtarzalne środowisko są tylko elementem układanki:
- blue-green – dwa równoległe środowiska (blue i green), jedno aktywne; nowy obraz wchodzi na nieaktywne, potem ruch przełącza się jednym ruchem,
- shadow – nowy model dostaje kopię ruchu produkcyjnego, ale jego odpowiedzi nie są używane; służą wyłącznie do porównania i analizy,
- canary – nowa wersja modelu obsługuje na początku mały procent ruchu (np. 1%), a udział rośnie wraz z obserwacją metryk.
W każdym z tych scenariuszy kluczowe jest to, że środowisko modelu jest opisane i kontrolowane. Przy blue-green można łatwo wrócić do poprzedniego obrazu; przy canary porównać dokładnie, jaki impact ma zmiana wersji CUDA czy biblioteki preprocessingowej. Bez Dockera i ustandaryzowanej konfiguracji przenoszenie modeli przypomina operacje na żywym organizmie bez dokumentacji.
Modele w trybie batch vs online – konsekwencje dla środowiska
Ten sam model fraudowy może działać w dwóch trybach: online (API udzielające odpowiedzi w milisekundach) i batch (nocne przeliczenia wszystkich transakcji). Teoretycznie zależności te same; praktycznie – dwa zupełnie różne „światy” środowiskowe.
Dla inferencji online priorytetem jest:
- szybki start kontenera i niski narzut pamięci,
- stabilność i przewidywalność opóźnień,
- prosty interfejs (HTTP/gRPC) i integracja z systemem monitoringu.
Dlatego obraz bywa „odchudzony”: bez narzędzi do trenowania, bez notebooków, bez ciężkich debugerów. Nawet jeśli w Condzie dewelopera te dodatki są wygodne, do obrazu produkcyjnego nie muszą trafić.
W batchu liczy się co innego: przepustowość, integracja z systemem kolejkowania zadań, czasami obsługa rozproszonego przetwarzania (Spark, Dask). Tu z kolei środowisko może zawierać więcej elementów (np. klienta Hadoopa, narzędzia do komunikacji z schedulerem), ale nie musi być optymalizowane pod opóźnienia per request.
Sensowne jest utrzymywanie dwóch wariantów obrazów z tym samym modelem, ale innym „otoczeniem”: model-serving i model-batch. Oba dzielą warstwę z samym modelem i jego zależnościami ML, różnią się warstwą z aplikacją i konfiguracją startową.
Kiedy uprościć, zamiast walczyć z perfekcyjną reprodukowalnością
Na koniec pojawia się też bardziej przyziemne pytanie: czy zawsze trzeba dążyć do idealnie identycznego środowiska między laptopem, stagingiem i produkcją? W małych projektach albo w eksperymentalnych fazach próba odwzorowania całej piramidy potrafi sparaliżować pracę.
Czasem rozsądniej jest ustalić proste zasady:
- jeden oficjalny obraz „dev” używany przez wszystkich do eksperymentów (np. uruchamiany przez VS Code Remote Containers albo JupyterHub),
- jeden obraz „prod base”, z którego wychodzą wszystkie usługi inferencyjne,
- jasne granice: co jest częścią środowiska (Docker + lockfile), a co konfiguracją per środowisko (sekrety, adresy usług, limity zasobów).
Jeśli zespół potrafi uruchomić ten sam obraz na laptopie, w stagingu i w produkcji, to większość pułapek zależności znika. Reszta sprowadza się do świadomego zarządzania tym, co i kiedy do obrazu trafia – niezależnie od tego, czy bazą jest Conda, czy „czysty” Python w Dockerze.
Najczęściej zadawane pytania (FAQ)
Jak poprawnie przenieść model z laptopa (Windows + Anaconda) na serwer Linux, żeby dostać te same wyniki?
Najczęstszy scenariusz wygląda tak: model na Windowsie śmiga, a na Linuksie pojawiają się błędy importu albo inne predykcje. Klucz tkwi w tym, żeby nie kopiować „gołego” kodu i pliku modelu, tylko całe środowisko – od wersji Pythona po biblioteki natywne.
Praktyczny schemat jest prosty: zbuduj środowisko w Condzie na laptopie (environment.yml z wersją Pythona i kluczowych pakietów), a na serwerze odtwórz je 1:1. Następnie oprzyj Dockera na tym samym Pythonie i tym samym zestawie bibliotek, zamiast instalować wszystko ręcznie przez pip install na systemowym Pythonie. Im mniej „doinstaluję tylko tę jedną bibliotekę”, tym bliżej będziesz identycznych wyników.
Czy wystarczy plik requirements.txt, żeby przenieść model ML między środowiskami?
Na początku kusi, żeby zrobić pip freeze > requirements.txt i uznać sprawę za załatwioną. W praktyce taki plik opisuje tylko część układanki: pakiety Pythona. Nie ma w nim wersji Pythona, informacji o systemie, glibc, BLAS-ie czy CUDA, które często decydują o tym, czy model wstanie i czy policzy to samo.
Lepszym podejściem jest połączenie: opis środowiska Conda (z Pythonem i kluczowymi bibliotekami) + konkretny obraz Dockera jako baza. Requirements.txt może być elementem pomocniczym, ale nie powinien być jedynym źródłem prawdy o środowisku. Przy ML „działa” i „działa tak samo” to dwie różne historie.
Jak używać Condy i Dockera razem przy deploymencie modelu?
Częsty obrazek: data scientist pracuje w Condzie lokalnie, a zespół DevOps chce czystego Dockera na produkcji. Da się to pogodzić, o ile Conda służy do wygodnego developmentu, a Docker staje się główną jednostką deployu.
Sprawdza się takie podejście: najpierw stabilizujesz środowisko Conda (environment.yml), potem tworzysz Dockerfile, który buduje obraz z tym samym Pythonem i tymi samymi kluczowymi pakietami. Conda może działać również wewnątrz Dockera, ale ważniejsze jest, żeby wersje Pythona, frameworków (PyTorch, TensorFlow) i krytycznych bibliotek numerycznych były zgrane. Wtedy laptop, serwer firmowy i chmura działają w praktyce na tym samym stosie.
Dlaczego ten sam model daje inne predykcje po przeniesieniu na inny serwer?
Na oko wszystko jest „takie samo”: ten sam kod, ten sam plik z wagami, te same dane wejściowe, a wyniki różne. Źródło problemu zwykle leży poziom niżej – w innej wersji numpy, innym BLAS-ie (MKL vs OpenBLAS), minimalnie innej wersji PyTorcha czy TensorFlow, innym kompilatorze albo różnicach w CUDA/cuDNN.
Jeśli chcesz zminimalizować takie rozjazdy, traktuj środowisko jako część artefaktu: przypnij wersję Pythona, paczek ML, bibliotek numerycznych i sterowników GPU, a całość zamknij w obrazie Dockera. Gdy te warstwy są spójne, różnice numeryczne albo znikają, albo mieszczą się w granicach akceptowalnych odchyleń.
Conda vs Docker w Machine Learning – czego używać do przenoszenia modeli?
W praktyce oba narzędzia rozwiązują trochę inne problemy. Conda świetnie nadaje się do pracy lokalnej: izoluje Pythona, zarządza pakietami (także natywnymi) i pozwala łatwo odtworzyć środowisko na innym laptopie czy serwerze. Docker idzie głębiej – pakuje cały system operacyjny, bibliotekę systemową, Pythona, pakiety i kod w jeden obraz.
Dobry kompromis dla małych i średnich zespołów to: Conda do developmentu i eksploracji, Docker jako standardowy sposób deployu na test i produkcję. Wtedy to, co sprawdziłeś w Condzie, finalnie ląduje w kontrolowanym obrazie Dockera, który można bezboleśnie przenosić między maszynami, klastrami i chmurami.
Jak opisać środowisko ML, żeby dało się je odtworzyć za pół roku?
Typowy ból: wracasz po kilku miesiącach do projektu, serwer został przeinstalowany, a nikt nie pamięta, co było zainstalowane „na boku”. Żeby tego uniknąć, środowisko musi być elementem wersjonowanego artefaktu, a nie wiedzą „w głowie seniora”.
Praktyczny zestaw obejmuje: plik environment.yml (lub odpowiednik w pipenv/poetry) z wersją Pythona i pakietów, Dockerfile z jasno wskazanym obrazem bazowym (np. konkretny tag CUDA + cuDNN), informację o wersjach sterowników GPU i ewentualną dokumentację drobnych zależności systemowych. Taki pakiet pozwala odtworzyć trening i inference krok po kroku, bez zgadywania i wielogodzinnego debugowania.
Jakie są najczęstsze pułapki zależności przy przenoszeniu modeli ML?
Najbardziej zdradliwe są drobne różnice, których na początku nikt nie kojarzy z problemem. Na listę podejrzanych zwykle trafiają: inna wersja Pythona (3.8 vs 3.11), wymieszane środowiska (systemowy Python + Conda + globalne pip), konflikt PyTorch/TensorFlow o sterowniki GPU, różne wersje numpy/pandas oraz biblioteki BLAS skompilowane z innymi flagami.
Dobrym nawykiem jest ograniczenie „ręcznych napraw”: żadnych pip install bez aktualizacji plików konfiguracyjnych, żadnego instalowania paczek systemowych „na żywo” na produkcji bez zapisania tego w Dockerfile. Im bardziej proces jest deklaratywny (Conda + Docker + infra as code), tym mniej niespodzianek przy migracjach i odtwarzaniu środowisk.






