অধ্যায় 1 · পেশাদারের মতো Git ও GitHub
AI প্রজেক্টে Git: ডেটা, মডেল, LFS, DVC ও ফাঁস হওয়া সিক্রেট
- পৃষ্ঠা 3 / 22
- 22 মিনিট পড়া
Git তৈরি হয়েছিল সোর্স কোডের জন্য: অনেক ছোট টেক্সট ফাইল, যার প্রতিবার কয়েকটা লাইন বদলায়। অথচ একটা AI প্রজেক্টে থাকে কয়েকশো মেগাবাইটের ডেটাসেট, প্রতিবার রিট্রেইন করলে পুরোপুরি বদলে যাওয়া মডেল ফাইল, আউটপুটে ভরা নোটবুক, হুবহু আবার বানাতে হবে এমন একটা এনভায়রনমেন্ট, আর মডেল প্রোভাইডারের API কি (key), যা কারও সঙ্গে শেয়ার করা চলবে না। এই সবকিছু অবুঝের মতো Git-এ ঢুকিয়ে দিলে রিপোজিটরি ধীর, বিশাল আর অ-পুনরুৎপাদনযোগ্য হয়ে যায়, আর সবচেয়ে খারাপ ক্ষেত্রে আপনার কি-গুলোর একটা পাবলিক কপি তৈরি হয়ে যায়।
এই পাতায় অভিজ্ঞ টিমগুলো যেভাবে AI রিপোজিটরি সাজায়, সেভাবে সাজানো শেখানো হবে: কী Git-এ যাবে, কী তার পাশে থাকবে (Git LFS, DVC), লকফাইল কীভাবে এনভায়রনমেন্টকে পুনরুৎপাদনযোগ্য করে, আর কোনো সিক্রেট কমিট হয়ে গেলে ঠিক কোন ক্রমে কী করতে হয়।
যা শিখবেন
- AI প্রজেক্টের পরিষ্কার ফোল্ডার কাঠামো আর
.gitignore, যাgit check-ignoreদিয়ে যাচাই করা হবে uv.lockদিয়ে পুনরুৎপাদনযোগ্য এনভায়রনমেন্ট, আর লকফাইল কেন কমিট করতে হয়- বড় ফাইল কেন Git-কে কষ্ট দেয়, আর Git LFS ও DVC কীভাবে আলাদা পথে তার সমাধান করে (নিজের হাতে বানানো একটা মিনি-DVC সহ)
- সাধারণ Git, LFS, DVC আর অন্য স্টোরেজের মধ্যে কীভাবে বেছে নেবেন
- হিস্ট্রির যেকোনো জায়গায় সিক্রেট খুঁজে বের করা, সেটা মুছে ফেলা, আর তার চেয়েও জরুরি ধাপটি: সেটা রোটেট করা
AI রিপোজিটরির কাঠামো
কোনো একক স্ট্যান্ডার্ড নেই, তবে যেসব AI রিপোজিটরি অনেক দিন টিকে থাকে, তার বেশির ভাগই একই জিনিসগুলো আলাদা করে রাখে:
sentiment-lab/
├── pyproject.toml dependencies (Git)
├── uv.lock exact versions of everything (Git)
├── .gitignore .gitattributes .env.example (Git)
├── src/sentiment_lab/ importable code: features, training, serving (Git)
├── notebooks/ exploration; outputs stripped before commit (Git)
├── prompts/ prompt templates, versioned like code (Git)
├── configs/ training and model settings, YAML (Git)
├── evals/ evaluation sets and scripts (Git, small sets)
├── tests/ (Git)
├── data/
│ ├── raw/ original data, never edited (DVC or a bucket, not Git)
│ └── processed/ produced by code, can be rebuilt (DVC or ignored)
├── models/ trained artefacts (LFS, DVC or a model registry)
└── .env real keys (never committed)নিয়মটা এই: Git রাখে মানুষ যা লেখে (কোড, কনফিগ, প্রম্পট, ছোট eval সেট, ডকুমেন্টেশন)। মেশিন যা বানায় বা যা বড়, সেগুলো হয় কোড দিয়ে আবার বানিয়ে নেওয়া হয়, নয়তো বড় ফাইলের জন্য তৈরি কোনো জায়গায় রাখা হয়, আর Git শুধু তার একটা ছোট রেফারেন্স রাখে।
AI প্রজেক্টের জন্য .gitignore
নাদিয়া প্রজেক্টটা একটা নতুন রিপোজিটরিতে শুরু করছে:
mkdir -p ~/projects/sentiment-lab
cd ~/projects/sentiment-lab
git init -q
mkdir -p src/sentiment_lab notebooks prompts data/raw data/processed models# file: .gitignore
# Python
__pycache__/
*.py[cod]
.venv/
.pytest_cache/
.ruff_cache/
# Secrets: commit .env.example, never .env
.env
.env.*
!.env.example
# Notebooks
.ipynb_checkpoints/
# Data and models: tracked with DVC or LFS, or rebuilt by code.
# Keep the folders (.gitkeep), DVC pointers (*.dvc) and DVC's own .gitignore files.
/data/raw/*
/data/processed/*
!/data/**/.gitkeep
!/data/**/*.dvc
!/data/**/.gitignore
/models/*.pkl
/models/*.joblib
/models/*.safetensors
/models/*.onnx
# Experiment tracking and caches
mlruns/
wandb/
.cache/
# Editors and OS
.vscode/
.idea/
.DS_Storeপ্যাটার্ন সূক্ষ্মভাবে ভুল হওয়া খুব সহজ, তাই সেগুলো পরীক্ষা করুন। git check-ignore -v দেখায় কোন ফাইলের কোন লাইনের কোন প্যাটার্ন একটা পাথকে ইগনোর করছে; -n দিলে যেসব পাথ ইগনোর হয়নি সেগুলোও আসে (সামনে :: দিয়ে):
touch .env .env.example .env.production data/raw/.gitkeep data/raw/reviews.csv models/model.joblib notebooks/explore.ipynb
git check-ignore -v -n .env .env.example .env.production data/raw/.gitkeep data/raw/reviews.csv models/model.joblib notebooks/explore.ipynb.gitignore:10:.env .env
.gitignore:12:!.env.example .env.example
.gitignore:11:.env.* .env.production
.gitignore:21:!/data/**/.gitkeep data/raw/.gitkeep
.gitignore:19:/data/raw/* data/raw/reviews.csv
.gitignore:25:/models/*.joblib models/model.joblib
:: notebooks/explore.ipynbনিয়মগুলোর ক্রম খেয়াল করুন। .env.* প্যাটার্ন .env.example-কেও ইগনোর করত; পরের লাইনের !.env.example সেটা ফিরিয়ে আনে। /data/raw/* ফোল্ডারটাকে নয়, তার ভেতরের জিনিসগুলোকে ইগনোর করে, কারণ ইগনোর করা ফোল্ডারের ভেতরের কোনো ফাইলকে Git আবার "ইগনোর নয়" করতে পারে না; এ জন্যই খালি .gitkeep ফাইলটা দিয়ে ফোল্ডারটা রিপোজিটরিতে রাখা যায়।
নোটবুক টেক্সট (JSON), কিন্তু সে ফাইলে প্রতিটা আউটপুট, ছবি আর এরর জমিয়ে রাখে। ফলে ডিফ পড়া যায় না, আর যে ডেটা প্রিন্ট করেছিলেন তা ফাঁসও হতে পারে। বেশির ভাগ টিম কমিটের আগে আউটপুট মুছে ফেলে (nbstripout বা pre-commit হুক দিয়ে), অথবা Jupytext দিয়ে নোটবুক .py ফাইল হিসেবে রাখে।
পুনরুৎপাদনযোগ্য এনভায়রনমেন্ট: uv.lock
AI-তে "আমার মেশিনে তো চলে" মানে সাধারণত NumPy, scikit-learn বা কোনো টোকেনাইজারের ভার্সন আলাদা। pyproject.toml বলে আপনি কীসের ওপর নির্ভর করেন (scikit-learn>=1.9); আর uv.lock প্রতিটা প্ল্যাটফর্মের জন্য প্রতিটা প্যাকেজের, এমনকি আপনার ডিপেন্ডেন্সির ডিপেন্ডেন্সিরও, সঠিক ভার্সন ও ফাইল হ্যাশ লিখে রাখে:
uv init -q --bare
uv add -q scikit-learn joblib
cat pyproject.toml
grep -c '^\[\[package\]\]' uv.lock[project]
name = "sentiment-lab"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"joblib>=1.6.0",
"scikit-learn>=1.9.1",
]
8সরাসরি ডিপেন্ডেন্সি দুটো, আর লক করা এন্ট্রি আটটা: প্রজেক্ট নিজে, scikit-learn আর joblib, এবং এই দুটো যে পাঁচটা প্যাকেজ টেনে আনে (NumPy, SciPy ও আরও কয়েকটা), প্রতিটা পিন করা। দুটো ফাইলই কমিট করুন। তখন টিমের কেউ, একটা CI জব বা Docker বিল্ড uv sync --locked চালায়, যা লকফাইলে যা লেখা ঠিক তা-ই ইনস্টল করে, আর pyproject.toml ও uv.lock না মিললে চুপচাপ আপডেট না করে ফেইল করে:
uv sync -q --locked
git add .gitignore pyproject.toml uv.lock data/raw/.gitkeep
git commit -q -m "Start sentiment-lab: layout, .gitignore, locked dependencies"
git ls-files.gitignore
data/raw/.gitkeep
pyproject.toml
uv.lock.env, CSV, মডেল, .venv/: কোনোটাই Git-এ নেই। pip দিয়ে একই কাজ করতে হলে পিন করা ভার্সন ও হ্যাশসহ একটা requirements.txt লাগে (pip install --require-hashes -r requirements.txt, যেটা uv pip compile --generate-hashes বা pip-tools দিয়ে বানানো যায়)। Python ভার্সনও লক করুন (requires-python বা একটা .python-version ফাইল); এই কোর্সে স্ট্যান্ডার্ড ৩.১৪।
বড় ফাইল কেন Git-কে কষ্ট দেয়
Git প্রতিটা ফাইলের প্রতিটা ভার্সন চিরকাল রেখে দেয়, আর প্রতিটা ক্লোন সবগুলো নামিয়ে আনে। ৫ MB-র একটা মডেল তিনবার রিট্রেইন করে তারপর মুছে ফেললে কী হয়:
for v in 1 2 3; do
python -c "import random; random.seed($v); open('models/sentiment.bin', 'wb').write(random.randbytes(5_000_000))"
git add models/sentiment.bin
git commit -q -m "Model v$v"
done
git rm -q models/sentiment.bin
git commit -q -m "Remove the model from Git"
git gc -q
git count-objects -vH | grep size-packsize-pack: 14.31 MiBমডেল ওয়ার্কিং ফোল্ডার থেকে চলে গেছে, তবু রিপোজিটরিতে তিনটা ভার্সনই আছে, ১৪ MB, যা প্রতিটা git clone চিরকাল নামাবে। মডেলের ওয়েট প্রায় কম্প্রেসই হয় না, আর দুটো ভার্সনের মধ্যে প্রায় কিছুই মেলে না, তাই ডেল্টা কম্প্রেশনও কাজে আসে না। এবার কল্পনা করুন ৪০০ MB-র একটা মডেল প্রতি সপ্তাহে রিট্রেইন হচ্ছে। GitHub ৫০ MB-র বেশি ফাইলে সতর্ক করে, ১০০ MB-র বেশি হলে ফিরিয়ে দেয়, আর রিপোজিটরি কয়েক গিগাবাইটের অনেক নিচে রাখতে বলে। ফাইল মুছে দিলে লাভ হয় না; শুধু হিস্ট্রি নতুন করে লিখলে হয় (সিক্রেটের ক্ষেত্রেও তা-ই, নিচে দেখুন)।
পাতার বাকি অংশে এই চারটা কমিট যেন আমাদের পিছু না নেয়, সে জন্য সেগুলো বাতিল করছি (এটা আমাদের নিজের, এখনো প্রকাশ না করা হিস্ট্রি, তাই এটা নতুন করে লেখা ঠিক আছে):
git reset -q --hard HEAD~4
git reflog expire --expire=now --all
git gc -q --prune=now
git count-objects -vH | grep size-packsize-pack: 2.67 KiBGit LFS: বড় ফাইল বাইরে, পয়েন্টার ভেতরে
Git LFS (Large File Storage, ভার্সন ৩.x) একটা Git এক্সটেনশন। কোন প্যাটার্নগুলো বড় তা আপনি বলে দেন; ওইরকম কোনো ফাইল git add করলে একটা ক্লিন ফিল্টার আসল কনটেন্ট .git/lfs/objects-এ রাখে আর Git-এ বসায় ছোট্ট একটা পয়েন্টার ফাইল। push করার সময় কনটেন্ট আলাদা একটা LFS সার্ভারে যায় (GitHub-এ এটা বিল্ট-ইন আছে); checkout-এর সময় একটা স্মাজ ফিল্টার কনটেন্ট নামিয়ে এনে আবার জায়গামতো বসায়। আপনার ওয়ার্কিং ফোল্ডার স্বাভাবিক দেখায়; Git-এর হিস্ট্রিতে শুধু পয়েন্টার থাকে।
Git LFS একটা আলাদা প্রোগ্রাম (
git lfs versionচালিয়ে দেখুন আছে কি না; git-lfs.com বা আপনার প্যাকেজ ম্যানেজার থেকে ইনস্টল করুন; Windows-এর Git for Windows-এ এটা আগে থেকেই থাকে)। এই পাতার কোড যে পরিবেশে যাচাই করা হয়েছে, সেখানে এটা ইনস্টল করা ছিল না, তাই নিচেরgit lfsকমান্ডগুলো Git LFS 3.x যে আউটপুট দেয় সেটা দিয়ে দেখানো হয়েছে, কিন্তু এখানে চালানো হয়নি।
git lfs install # প্রতি মেশিনে একবার: ~/.gitconfig-এ LFS ফিল্টার নিবন্ধন করেতারপর প্রতিটা রিপোজিটরিতে একবার করে বলে দিন LFS কোন ফাইলগুলো সামলাবে:
git lfs track "models/*.bin"
cat .gitattributesTracking "models/*.bin"
models/*.bin filter=lfs diff=lfs merge=lfs -text.gitattributes-ই আসল জিনিস: এটা কমিট করা হয়, তাই প্রতিটা ক্লোনে একই নিয়ম প্রযোজ্য হয়। -text বাইনারি ফাইলের লাইন-এন্ডিং বদলানো থেকে Git-কে আটকায়। এরপর আপনার কাজের ধরন একটুও বদলায় না: git add models/sentiment.bin, git commit, git push। Git আসলে যা রাখে তা এরকম একটা পয়েন্টার। আমাদের মডেলের ঠিক পয়েন্টারটা সাধারণ Python দিয়েই হিসাব করা যায়, কারণ সেটা শুধু কনটেন্টের SHA-256 আর সাইজ:
import hashlib
import random
random.seed(3)
content = random.randbytes(5_000_000) # ওপরের মডেল v3
pointer = (
"version https://git-lfs.github.com/spec/v1\n"
f"oid sha256:{hashlib.sha256(content).hexdigest()}\n"
f"size {len(content)}\n"
)
print(pointer, end="")
print(len(pointer.encode()), "bytes in Git instead of", len(content))version https://git-lfs.github.com/spec/v1
oid sha256:8e220a7fd1cc035ec59dfdf85ee3f995c70c62f28ee6f66bd6ed45877f36ea65
size 5000000
132 bytes in Git instead of 5000000কাজের কমান্ড, সবই আসল Git LFS সাবকমান্ড:
| কমান্ড | কী করে |
|---|---|
git lfs ls-files | বর্তমান কমিটের যেসব ফাইল LFS-এ রাখা আছে তার তালিকা |
git lfs pull | বর্তমান checkout-এর জন্য LFS কনটেন্ট নামায় (যেমন GIT_LFS_SKIP_SMUDGE=1 দিয়ে ক্লোন করার পর) |
git lfs migrate import --include="*.bin" --everything | আগে থেকে থাকা হিস্ট্রি নতুন করে লিখে পুরোনো বড় ফাইলগুলোকে পয়েন্টার বানায় (এটা হিস্ট্রি নতুন করে লেখা: টিমের সঙ্গে আগে সমন্বয় করুন, তারপর force-push) |
git lfs prune | আর দরকার নেই এমন পুরোনো লোকাল LFS কনটেন্ট মুছে দেয় |
git lfs lock models/sentiment.bin | এমন ফাইল লক করে যা কেউ মার্জ করতে পারে না (.gitattributes-এ lockable লাগে) |
খরচ: GitHub-এর ফ্রি প্ল্যানে প্রতি মাসে ১০ GiB LFS স্টোরেজ আর ১০ GiB ব্যান্ডউইথ থাকে (পুরোনো টিউটোরিয়ালে ১ GB লেখা থাকে; সেটা এখন সেকেলে)। যে প্রতিটা ক্লোন আর CI রান ফাইলগুলো নামায়, তার জন্যই ব্যান্ডউইথ গোনা হয়, আর এখানেই টিমগুলো চমকে যায়: প্রতি push-এ ২ GB মডেল checkout করে এমন একটা CI জব পাঁচ রানেই মাসের কোটা শেষ করে ফেলে। বর্তমান সীমা GitHub-এর বিলিং পাতায় দেখে নিন, আর CI-তে LFS ফাইল শুধু সেই জবগুলোতে আনুন যাদের সত্যিই লাগে (actions/checkout-এ lfs: false-ই ডিফল্ট)।
DVC: Git-এর পাশে ডেটার ভার্সনিং
DVC (Data Version Control, ৩.x) একই পয়েন্টারের ধারণাকে আরও এগিয়ে নেয়। কনটেন্ট যায় একটা ক্যাশে এবং আপনার নিজের স্টোরেজে (S3, Google Cloud Storage, Azure, SSH, শেয়ার করা ড্রাইভ), GitHub-এ নয়; Git-এ থাকে ছোট .dvc ফাইল। এর ওপরে DVC পাইপলাইন (prepare → train → evaluate) বর্ণনা করতে পারে, ফলে ডেটা বা কোড বদলালে কোন আউটপুট আবার বানাতে হবে তা সে জানে। প্রজেক্টটি ২০২৫ সালের নভেম্বরে Iterative থেকে lakeFS (Treeverse)-এর হাতে গেছে; এটা এখনো মেইনটেইন করা হয় এবং বহুল ব্যবহৃত, তবে উন্নয়ন ধীর হয়ে গেছে, তাই একে দ্রুত-বদলানো টুল না ভেবে নির্ভরযোগ্য একটা টুল হিসেবে দেখুন।
ধারণাটা, ৩০ লাইনের Python-এ
আসল DVC ব্যবহার করার আগে তার মূল ধারণাটা নিজে বানিয়ে দেখুন। add() ফাইলটাকে তার MD5 হ্যাশের নামে ক্যাশে কপি করে (DVC ৩ যে files/md5/ab/cdef… কাঠামো ব্যবহার করে, হুবহু তা-ই), DVC-র ফরম্যাটে একটা .dvc পয়েন্টার লেখে, আর আসল ফাইলটা Git-কে দিয়ে ইগনোর করায়। checkout() একটা পয়েন্টার যে কনটেন্টের নাম বলে সেটা ফিরিয়ে আনে:
import hashlib
import shutil
from pathlib import Path
CACHE = Path(".minidvc/cache/files/md5")
def md5_of(path):
h = hashlib.md5()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def add(path):
"""ফাইলটা কনটেন্ট হ্যাশ দিয়ে ক্যাশে রাখে, Git-এর জন্য পয়েন্টার লেখে, আসল ফাইল ইগনোর করায়।"""
path = Path(path)
digest = md5_of(path)
cached = CACHE / digest[:2] / digest[2:]
cached.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(path, cached)
pointer = path.with_name(path.name + ".dvc")
pointer.write_text(f"outs:\n- md5: {digest}\n size: {path.stat().st_size}\n"
f" hash: md5\n path: {path.name}\n")
ignore = path.parent / ".gitignore"
rule = f"/{path.name}\n"
if rule not in (ignore.read_text() if ignore.exists() else ""):
with open(ignore, "a") as f:
f.write(rule)
def checkout(pointer):
"""একটা .dvc পয়েন্টার যে কনটেন্টের নাম বলে তা ফিরিয়ে আনে।"""
pointer = Path(pointer)
digest = pointer.read_text().split("md5: ")[1].split()[0]
shutil.copyfile(CACHE / digest[:2] / digest[2:], pointer.with_name(pointer.name[:-4]))ডেটাসেট: ১,০০০টা লেবেল করা রিভিউ। ভার্সন ১ যোগ করা হলো আর তার পয়েন্টার কমিট হলো:
import csv
def write_reviews(n):
with open("data/raw/reviews.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["id", "text", "label"])
for i in range(n):
w.writerow([i, f"review number {i}", "positive" if i % 3 else "negative"])
write_reviews(1000)
add("data/raw/reviews.csv")
print(Path("data/raw/reviews.csv.dvc").read_text(), end="")
print(Path("data/raw/.gitignore").read_text(), end="")outs:
- md5: 10bbe7d39438b5716cf45052a9fb0e39
size: 31795
hash: md5
path: reviews.csv
/reviews.csvecho ".minidvc/" >> .gitignore
git add .gitignore data/raw/reviews.csv.dvc data/raw/.gitignore
git commit -q -m "Track reviews.csv v1 (1000 rows)"
git status --short --ignored data!! data/raw/reviews.csvGit একশো বাইটেরও কম একটা পয়েন্টার ট্র্যাক করে; ৩১ KB-র CSV নিজে ইগনোর করা। এবার ডেটা টিম আরও ৫০০টা রিভিউ যোগ করল, আর নাদিয়া ভার্সন ২ কমিট করছে:
write_reviews(1500)
add("data/raw/reviews.csv")git commit -q -am "Track reviews.csv v2 (1500 rows)"
git log --oneline -2
git diff HEAD~1 -- data/raw/reviews.csv.dvc3483dd4 Track reviews.csv v2 (1500 rows)
389fbfc Track reviews.csv v1 (1000 rows)
diff --git a/data/raw/reviews.csv.dvc b/data/raw/reviews.csv.dvc
index 8c09b27..c188e9f 100644
--- a/data/raw/reviews.csv.dvc
+++ b/data/raw/reviews.csv.dvc
@@ -1,5 +1,5 @@
outs:
-- md5: 10bbe7d39438b5716cf45052a9fb0e39
- size: 31795
+- md5: bc72a9bc77a761b3cf4e81c669e04329
+ size: 48795
hash: md5
path: reviews.csvওই ডিফটাই Git-এ ডেটাসেটের পুরো ইতিহাস: প্রতি ভার্সনের জন্য একটা হ্যাশ আর একটা সাইজ। গত সপ্তাহের মডেলটা আবার বানাতে হলে পুরোনো পয়েন্টারে ফিরে যান, আর সেটা যে ডেটার নাম বলে তা ফিরিয়ে আনুন:
git checkout -q HEAD~1 -- data/raw/reviews.csv.dvccheckout("data/raw/reviews.csv.dvc")
print(sum(1 for _ in open("data/raw/reviews.csv")) - 1, "rows")
print(sorted(p.parent.name + p.name for p in CACHE.rglob("*") if p.is_file()))1000 rows
['10bbe7d39438b5716cf45052a9fb0e39', 'bc72a9bc77a761b3cf4e81c669e04329']দুটো ভার্সনই ক্যাশে আছে; পয়েন্টার একটাকে বেছে নেয়। আসল DVC আরও যা দেয় যা আমাদের খেলনা ভার্সনে নেই: ক্যাশ রিমোট স্টোরেজে push করা আর অন্য মেশিনে pull করা, বড় ফাইলের জন্য কপির বদলে লিংক, ডিরেক্টরি, পাইপলাইন আর মেট্রিক্স। এগোনোর আগে সবচেয়ে নতুন পয়েন্টার আর তার ডেটা ফিরিয়ে আনুন:
git checkout -q HEAD -- data/raw/reviews.csv.dvccheckout("data/raw/reviews.csv.dvc")
print(sum(1 for _ in open("data/raw/reviews.csv")) - 1, "rows")1500 rowsআসল DVC কমান্ড
DVC একটা Python প্যাকেজ (
uv add --dev dvcবাpip install dvc, আর ক্লাউড স্টোরেজের জন্যdvc[s3]-এর মতো একটা এক্সট্রা)। এই পাতা যে অফলাইন পরিবেশে যাচাই করা হয়েছে, সেখানে এটা পাওয়া যায়নি, তাই এই কমান্ডগুলো দেখানো হয়েছে, চালানো হয়নি।
pip install dvc
dvc init # .dvc/ ও .dvcignore বানায় এবং Git-এ স্টেজ করে
dvc add data/raw/reviews.csv # ক্যাশ + data/raw/reviews.csv.dvc + data/raw/.gitignore
git add data/raw/reviews.csv.dvc data/raw/.gitignore
git commit -m "Track reviews.csv with DVC"
dvc remote add -d storage s3://bitbyte-ml/dvc # অথবা একটা ফোল্ডার: dvc remote add -d storage /mnt/shared/dvc
git commit -am "Configure DVC remote"
dvc push # ক্যাশ করা ডেটা রিমোটে আপলোড করে
# টিমের কেউ, অথবা CI:
git clone https://github.com/bitbyte-ai/sentiment-lab.git
cd sentiment-lab
dvc pull # পয়েন্টারগুলো যে ডেটার নাম বলে তা নামায়
# ডেটাসেটের পুরোনো ভার্সনে ফিরে যেতে:
git checkout v1.0 -- data/raw/reviews.csv.dvc
dvc checkout data/raw/reviews.csv.dvcআসল DVC ৩ যে .dvc ফাইল লেখে তার গড়ন আমাদেরটার মতোই (outs, md5, size, hash: md5, path)। পাইপলাইন থাকে dvc.yaml-এ: প্রতিটা স্টেজে তার কমান্ড, ডিপেন্ডেন্সি আর আউটপুট লেখা থাকে, dvc repro শুধু সেই স্টেজগুলো আবার চালায় যাদের ইনপুট বদলেছে, আর dvc.lock কোন হ্যাশের জিনিস ব্যবহার হয়েছিল তা লিখে রাখে।
stages:
prepare:
cmd: python src/sentiment_lab/prepare.py
deps:
- src/sentiment_lab/prepare.py
- data/raw/reviews.csv
outs:
- data/processed/train.csv
train:
cmd: python src/sentiment_lab/train.py
deps:
- src/sentiment_lab/train.py
- data/processed/train.csv
params:
- train.C
outs:
- models/model.joblib
metrics:
- metrics.json:
cache: falseGit, LFS, DVC, নাকি অন্য কিছু?
| সাধারণ Git | Git LFS | DVC | |
|---|---|---|---|
| সবচেয়ে ভালো কাজে লাগে | কোড, কনফিগ, প্রম্পট, ছোট eval সেট (কয়েক MB-র কম) | কোডের সঙ্গে থাকা উচিত এমন কয়েকটা বড় বাইনারি ফাইল: ছোট মডেল, ছবি, ফিক্সচার | ঘন ঘন বদলানো ডেটাসেট ও মডেল; পুনরুৎপাদনযোগ্য পাইপলাইন |
| কনটেন্ট থাকে কোথায় | রিপোজিটরিতেই | আপনার Git হোস্টের LFS সার্ভারে | আপনার স্টোরেজে (S3, GCS, Azure, SSH, একটা ফোল্ডার) |
| বাড়তি টুল | নেই | git lfs | dvc (Python) |
| GitHub Free-তে খরচ | ফ্রি | ১০ GiB স্টোরেজ + প্রতি মাসে ১০ GiB ব্যান্ডউইথ | GitHub শুধু পয়েন্টার রাখে; স্টোরেজের খরচ আপনার |
| পাইপলাইন ও মেট্রিক্স | না | না | হ্যাঁ (dvc.yaml, dvc repro, dvc metrics) |
| আংশিক ডাউনলোড | না (shallow/partial clone কিছুটা সাহায্য করে) | হ্যাঁ (--include, smudge বাদ দেওয়া) | হ্যাঁ (কোনো একটা পাথ dvc pull) |
বড় পরিসরে টিমগুলো আরও ব্যবহার করে: মডেল ও ডেটাসেটের জন্য Hugging Face Hub রিপোজিটরি (Git-ভিত্তিক, বড় ফাইলের জন্য তাদের Xet স্টোরেজ ব্যাকএন্ড সহ), lakeFS (ডেটা লেকের ওপর Git-এর মতো ব্রাঞ্চ), ভার্সনিং চালু করা সাধারণ অবজেক্ট স্টোরেজের সঙ্গে Git-এ একটা ম্যানিফেস্ট ফাইল, এবং মডেল রেজিস্ট্রি (MLflow, Weights & Biases আর্টিফ্যাক্ট, ক্লাউড ML প্ল্যাটফর্ম), যারা মডেলকে তার মেট্রিক্স ও উৎপত্তিসহ ভার্সন করে। নীতিটা সব জায়গায় একই: Git রাখে ছোট, রিভিউ করা যায় এমন একটা রেফারেন্স; বাইটগুলো থাকে অন্য কোথাও।
ফাঁস হওয়া সিক্রেট
AI প্রজেক্টে সাধারণ প্রজেক্টের চেয়ে বেশি কি থাকে: মডেল প্রোভাইডার, ভেক্টর ডেটাবেস, Hugging Face, ক্লাউড স্টোরেজ। বটগুলো সারাক্ষণ পাবলিক GitHub স্ক্যান করে, আর ফাঁস হওয়া LLM কি কয়েক মিনিটের মধ্যে ব্যবহার করে ফেলে, সাধারণত বিল বাড়ানোর জন্য। ক্লাসিক ভুলটা ধাপে ধাপে দেখা যাক:
cat > src/sentiment_lab/config.py <<'EOF'
MODEL = "gpt-5-mini"
OPENAI_API_KEY = "sk-proj-FAKEa1b2c3d4e5f6g7h8i9j0"
EOF
git add src/sentiment_lab/config.py
git commit -q -m "Add LLM config"
# ... GitHub-এ push হলো; পরদিন টিমের একজন কি-টা দেখতে পেল ...
cat > src/sentiment_lab/config.py <<'EOF'
import os
MODEL = "gpt-5-mini"
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
EOF
git commit -q -am "Read the API key from the environment"
grep -c "sk-proj" src/sentiment_lab/config.py0বর্তমান ফাইলটা পরিষ্কার। হিস্ট্রি পরিষ্কার নয়।
হিস্ট্রির যেকোনো জায়গায় সিক্রেট খোঁজা
Git-এর তিনটা টুল বর্তমান ফাইল নয়, হিস্ট্রি জুড়ে খোঁজে:
git log --oneline -S "sk-proj-"
git log --oneline -G "sk-[A-Za-z0-9_-]{20,}"
git grep -n "sk-proj-" $(git rev-list --all)6337f52 Read the API key from the environment
b0525ca Add LLM config
6337f52 Read the API key from the environment
b0525ca Add LLM config
b0525ca83816a1883d43688dedd9c31fd5626396:src/sentiment_lab/config.py:2:OPENAI_API_KEY = "sk-proj-FAKEa1b2c3d4e5f6g7h8i9j0"-S "text"("পিকঅ্যাক্স") সেই কমিটগুলো দেখায় যেখানে ওই টেক্সটের সংখ্যা বদলেছে: যে কমিট সেটা যোগ করেছে আর যে সেটা সরিয়েছে। ডিফ দেখতে-pযোগ করুন।-G regexসেই কমিটগুলো দেখায় যাদের ডিফে রেগুলার এক্সপ্রেশনের সঙ্গে মেলে এমন কোনো যোগ বা বাদ দেওয়া লাইন আছে: সিক্রেটটা হুবহু জানা না থাকলে, শুধু তার আকার জানা থাকলে এটা কাজে লাগে।git grep text $(git rev-list --all)প্রতিটা কমিটের পুরো কনটেন্টে খোঁজে এবংcommit:file:line:textআকারে ছাপে।
নির্দিষ্ট স্ক্যানার টুলগুলো একই কাজ করে চেনা কি-ফরম্যাটের শত শত প্যাটার্ন আর এন্ট্রপি চেক দিয়ে: gitleaks (gitleaks git -v পুরো হিস্ট্রি স্ক্যান করে), TruffleHog (পাওয়া কি এখনো চালু আছে কি না তাও যাচাই করতে পারে), এবং GitHub-এর নিজস্ব secret scanning, যা পাবলিক রিপোজিটরির জন্য চালু ও ফ্রি।
প্রথম ধাপ, সবসময়: কি রোটেট করুন
একটা কি এমন রিমোটে push হয়ে গেলে যেখানে অন্য কেউ পড়তে পারে, ধরে নিন সেটা কম্প্রোমাইজড। হিস্ট্রি পরিষ্কার করলে ফাঁসটা ফেরত আসে না: ফর্ক, ক্লোন, CI লগ, ক্যাশ আর বটগুলোর কাছে ওটা আগেই চলে গেছে। তাই ক্রমটা নির্দিষ্ট:
- কি বাতিল (revoke) বা রোটেট করুন প্রোভাইডারের ড্যাশবোর্ডে, আর নতুন কি
.env-এ বা আপনার সিক্রেট ম্যানেজারে রাখুন। শুধু এটুকুই ফাঁসটাকে নিরীহ করে দেয়। - ক্ষতি যাচাই করুন: প্রোভাইডারের ব্যবহার ও বিলিং পাতা, অডিট লগ; খরচের সীমা ঠিক করুন বা কমিয়ে দিন।
- হিস্ট্রি পরিষ্কার করুন, যদি রিপোজিটরি পাবলিক হয় বা সিক্রেটটা কি ছাড়াও স্পর্শকাতর হয় (পাসওয়ার্ড, ব্যক্তিগত ডেটা)।
- পরেরবার ঠেকান:
.gitignore-এ.env, push protection, pre-commit স্ক্যানার।
এখনো push হয়নি: লোকালিই ঠিক করুন
সিক্রেট সহ কমিটটা যদি আপনার মেশিন ছেড়ে না গিয়ে থাকে, ঠিক করা সস্তা। ধরা যাক নাদিয়া একটা Hugging Face টোকেন কমিট করে ফেলল, সঙ্গে সঙ্গে খেয়াল করল, আর কমিটটা বাতিল করল:
echo 'HF_TOKEN = "hf_FAKEabcdefghijklmnopqrstuvwxyz"' >> src/sentiment_lab/config.py
git commit -q -am "Add HF token"
git reset -q --hard HEAD~1
git log --oneline -1
git reflog -36337f52 Read the API key from the environment
6337f52 HEAD@{0}: reset: moving to HEAD~1
c61a854 HEAD@{1}: commit: Add HF token
6337f52 HEAD@{2}: commit: Read the API key from the environmentকমিটটা ব্রাঞ্চ থেকে চলে গেছে, কিন্তু এখনো আছে: reflog সেটা মনে রাখে, আর গারবেজ কালেকশন সাধারণত কয়েক সপ্তাহ পরে যখন অপ্রাপ্য অবজেক্ট সরায়, তার আগ পর্যন্ত অবজেক্টগুলো .git-এ থাকে। কেউ .git ফোল্ডার কপি করলে (সহকর্মীকে পাঠানো একটা zip, COPY . . দিয়ে বানানো একটা Docker ইমেজ) টোকেনটাও কপি হয়ে যায়:
git show HEAD@{1}:src/sentiment_lab/config.py | grep HF_TOKEN
git reflog expire --expire=now --all
git gc -q --prune=now
git show HEAD@{1}:src/sentiment_lab/config.py
git cat-file -t c61a854HF_TOKEN = "hf_FAKEabcdefghijklmnopqrstuvwxyz"
fatal: log for HEAD is empty
fatal: Not a valid object name c61a854reflog এখন খালি, আর গারবেজ কালেকশন কমিটের অবজেক্টগুলো মুছে দিয়েছে: Git আর হ্যাশটা চেনেই না। এবার এটা সত্যিই এই রিপোজিটরি থেকে গেছে। ওপরের OpenAI কি-র ক্ষেত্রে, যেটা "push করা হয়েছিল", একই reset যথেষ্ট হতো না: রিমোট আর প্রতিটা ক্লোনের কাছে ওটা এখনো আছে।
push হয়ে গেছে: git filter-repo দিয়ে হিস্ট্রি নতুন করে লিখুন
git filter-repo হলো হিস্ট্রি থেকে ডেটা সরানোর জন্য GitHub ও Git প্রজেক্ট যে টুলের সুপারিশ করে (পুরোনো git filter-branch ধীর আর ভুলভাবে ব্যবহার করা সহজ; Git নিজেই তার বিরুদ্ধে সতর্ক করে)। এটা একটা একক Python স্ক্রিপ্ট, Git-এর অংশ নয়, আর এই পাতা যেখানে যাচাই করা হয়েছে সেখানে ইনস্টল ছিল না, তাই এই কমান্ডগুলো দেখানো হয়েছে, চালানো হয়নি। একটা একদম নতুন ক্লোনে কাজ করুন; filter-repo ক্লোন নয় এমন রিপোজিটরিতে চলতে অস্বীকার করে, তাই ভুল করে আপনার কাজের কপি নষ্ট করা যায় না:
pip install git-filter-repo
git clone https://github.com/bitbyte-ai/sentiment-lab.git sentiment-lab-clean
cd sentiment-lab-clean
# প্রতি লাইনে: যে টেক্সট খুঁজবে ==> তার বদলে যা বসবে। (==> ছাড়া লাইনের টেক্সট ***REMOVED*** হয়ে যায়।)
echo 'sk-proj-FAKEa1b2c3d4e5f6g7h8i9j0==>REMOVED-OPENAI-KEY' > ../replacements.txt
git filter-repo --sensitive-data-removal --replace-text ../replacements.txt
git log --oneline -S "sk-proj-" # এখন কিছুই ছাপে না
git push --force --mirror originপ্রতিটা কমিট থেকে একটা পুরো ফাইল সরাতে চাইলে (কমিট হয়ে যাওয়া .env, ব্যক্তিগত ডেটাসহ ডেটাসেট), ব্যবহার করুন git filter-repo --sensitive-data-removal --invert-paths --path .env। প্রথম বদলানো কমিট থেকে শুরু করে প্রতিটা কমিটের নতুন হ্যাশ হয়, তাই:
- আগে সমন্বয় করুন: সবাই নিজের কাজ push করুক, রিরাইটের পর নতুন করে ক্লোন করুক। পুরোনো কোনো ক্লোন থেকে push বা মার্জ করলে সিক্রেটটা সোজা ফিরে আসে।
- GitHub-এ পুরোনো কমিটগুলো ক্যাশ করা ভিউ আর পুল রিকোয়েস্ট রেফের মাধ্যমে পৌঁছানো যেতে পারে; GitHub-এর "Removing sensitive data from a repository" গাইডে লেখা আছে সেগুলো মুছে দেওয়ার জন্য GitHub Support-কে কীভাবে অনুরোধ করবেন। ফর্কগুলো নিজেদের কপি রেখে দেয়।
- BFG Repo-Cleaner (একটা মিরর ক্লোনে
java -jar bfg.jar --replace-text replacements.txt, তারপরgit reflog expire --expire=now --all && git gc --prune=now --aggressive) পুরোনো ও দ্রুততর বিকল্প; ডিফল্টে এটা সর্বশেষ কমিট ছোঁয় না, তাই আগে বর্তমান ফাইলগুলো ঠিক করে নিন।
প্রতিরোধ
GitHub-এ push protection push-গুলোতে চেনা কি-ফরম্যাট খোঁজে এবং আটকে দেয়; পাবলিক রিপোজিটরিতে push করলে আপনার অ্যাকাউন্টের জন্য এটা ডিফল্টে চালু। আটকে গেলে push GH013: Repository rule violations found এবং "Push cannot contain secrets" বার্তা দিয়ে ফেইল করে, ফাইল, লাইন ও কমিটের নাম বলে, আর ভুল সতর্কতা হলে বাইপাস করার একটা লিংক দেয়। লোকালি, স্ক্যানার চালানো একটা pre-commit হুক সিক্রেটটাকে আরও আগে, কমিট তৈরি হওয়ার আগেই ধরে (কঠিন অনুশীলনে আপনি একটা লিখবেন)। তার চেয়েও ভালো: Git যে ফাইল দেখতে পায় তাতে কোনো কি-ই না রাখা, বরং এনভায়রনমেন্ট ভেরিয়েবল, .env ফাইল ও ভার্চুয়াল এনভায়রনমেন্ট পাতায় যেমন শেখানো হয়েছিল, এনভায়রনমেন্ট থেকে পড়া।
বাস্তব জীবনে ব্যবহার
- একটা মডেল পুনরুৎপাদন। "প্রোডাকশনের মডেলটা কোন ডেটা আর কোন কোড দিয়ে বানানো?" এর উত্তর একটাই Git কমিট: কোড,
uv.lock, কনফিগ আর.dvcপয়েন্টার একসঙ্গে। মডেল রিলিজ করার সময় টিমগুলো ওই কমিটে ট্যাগ বসায়। - Hugging Face মডেল রিপোজিটরি আসলে বড়-ফাইল স্টোরেজসহ Git রিপোজিটরি; কোনো মডেল রিপোর
git clone-এ আপনি এই পাতায় দেখা পয়েন্টার ব্যবস্থাই দেখবেন। - ইনসিডেন্ট রেসপন্স। আসল ফাঁসগুলো ঠিক ওপরের ক্রমে সামলানো হয়। ইনসিডেন্ট চ্যানেলের প্রথম প্রশ্ন "কি-টা রোটেট হয়েছে?", "কমিটটা কি সরানো হয়েছে?" নয়।
- CI ক্যাশ। লকফাইল ক্যাশ কি-ও বটে:
uv.lockনা বদলালে CI ভার্চুয়াল এনভায়রনমেন্ট ফিরিয়ে আনে, ফলে বিল্ড দ্রুত হয় আর প্রতিবার একই হয়।
সাধারণ ভুল
- নতুন কমিটে বড় ফাইল বা সিক্রেট মুছে দিয়ে ভাবা যে সেটা চলে গেছে। হিস্ট্রি সেটা রেখে দেয়। সাইজের ক্ষেত্রে হিস্ট্রি নতুন করে লিখুন (
git lfs migrate, filter-repo); সিক্রেটের ক্ষেত্রে আগে রোটেট করুন। - লকফাইল কমিট না করা (বা
uv.lock-কে.gitignore-এ দেওয়া)। তখন প্রতিটা ইনস্টল নতুন করে রিজল্ভ হয়, আর দুটো মেশিন আলাদা ভার্সন পায়। এটা কমিট করুন, আর--lockedদিয়ে ইনস্টল করুন। - একটা ফোল্ডার ইগনোর করে তারপর তার ভেতরের একটা ফাইল রাখতে চাওয়া।
data/আর তার সঙ্গে!data/.gitkeepকাজ করে না; তার বদলেdata/*ইগনোর করুন। - মাইগ্রেট না করেই পরে LFS-এ ট্র্যাক করা।
git lfs trackশুধু তখন থেকে যোগ হওয়া ফাইলে প্রভাব ফেলে; পুরোনো বড় ব্লবgit lfs migrate importনা চালানো পর্যন্ত হিস্ট্রিতে থেকে যায়। - কি রোটেট করার আগে হিস্ট্রি পরিষ্কার করা। আপনি রিরাইটে ব্যস্ত থাকার সময় কি-টা প্রতিটা ক্লোন, ফর্ক আর বটের ডেটাবেসে বৈধ থাকে। সবসময় আগে রোটেট করুন।
নিজে চেষ্টা করুন
- সহজ:
.gitignore-এ এমন নিয়ম যোগ করুন যাতেnotebooks/outputs/-এর সবকিছু আর যেকোনো জায়গার*.parquetফাইল ইগনোর হয়, তারপরgit check-ignore -vদিয়ে দুটোই প্রমাণ করুন। - মাঝারি: মিনি-DVC ফাংশনগুলো ব্যবহার করে
status(pointer)লিখুন, যা ফাইলের বর্তমান MD5 আর পয়েন্টারের MD5 মিলিয়েunchangedবাmodifiedছাপে (dvc statusযা করে)। একটা সারি যোগ করার আগে ও পরে পরীক্ষা করুন। - কঠিন: একটা
pre-commitহুক লিখুন যা স্টেজ করা যোগ-হওয়া কোনো লাইনsk-[A-Za-z0-9_-]{20,}বাhf_[A-Za-z0-9]{30,}-এর সঙ্গে মিললে কমিট ফিরিয়ে দেয়, আর একটা নকল কি সহ কমিট আটকাচ্ছে তা প্রমাণ করুন। (ইঙ্গিত:git diff --cached -U0শুধু স্টেজ করা পরিবর্তন দেখায়।)
উত্তর
# সহজ
printf '/notebooks/outputs/\n*.parquet\n' >> .gitignore
mkdir -p notebooks/outputs data/processed
touch notebooks/outputs/plot.png data/processed/features.parquet
git check-ignore -v notebooks/outputs/plot.png data/processed/features.parquet.gitignore:39:/notebooks/outputs/ notebooks/outputs/plot.png
.gitignore:40:*.parquet data/processed/features.parquetdata/processed/features.parquet দুটো নিয়মের সঙ্গে মেলে, /data/processed/* আর *.parquet। একাধিক প্যাটার্ন মিললে শেষেরটাই সিদ্ধান্ত নেয়, আর -v সেটাই দেখায়।
# মাঝারি
def status(pointer):
pointer = Path(pointer)
recorded = pointer.read_text().split("md5: ")[1].split()[0]
current = md5_of(pointer.with_name(pointer.name[:-4]))
print(pointer.name[:-4], "unchanged" if current == recorded else "modified")
status("data/raw/reviews.csv.dvc")
with open("data/raw/reviews.csv", "a") as f:
f.write("1500,one more review,positive\n")
status("data/raw/reviews.csv.dvc")reviews.csv unchanged
reviews.csv modified# কঠিন
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
if git diff --cached -U0 | grep '^+' | grep -Eq 'sk-[A-Za-z0-9_-]{20,}|hf_[A-Za-z0-9]{30,}'; then
echo "pre-commit: this commit adds something that looks like an API key." >&2
echo "Put it in .env and read it with os.environ instead." >&2
exit 1
fi
EOF
chmod +x .git/hooks/pre-commit
echo 'KEY = "sk-proj-FAKEzzzzzzzzzzzzzzzzzzzzzz"' > src/sentiment_lab/keys.py
git add src/sentiment_lab/keys.py
git commit -q -m "Add keys"
echo "exit code: $?"pre-commit: this commit adds something that looks like an API key.
Put it in .env and read it with os.environ instead.
exit code: 1.git/hooks-এর হুক কমিট করা হয় না, শেয়ারও হয় না, তাই টিমগুলো pre-commit ফ্রেমওয়ার্কের মতো টুল (gitleaks-এর তৈরি হুকসহ) দিয়ে সেগুলো ইনস্টল করে, আর নিরাপত্তার শেষ জালটা হিসেবে সার্ভার-সাইড push protection-এর ওপর ভরসা রাখে।
সারসংক্ষেপ
- Git রাখে মানুষ যা লেখে; ডেটা ও মডেল যায় LFS, DVC বা অন্য স্টোরেজে, আর Git-এ থাকে ছোট পয়েন্টার।
git check-ignore -vদিয়ে.gitignoreপরীক্ষা করুন; ব্যতিক্রম লাগলে ফোল্ডারের ভেতরের জিনিস ইগনোর করুন;.envকখনো কমিট করবেন না।- সব জায়গায় একই এনভায়রনমেন্টের জন্য
uv.lockকমিট করুন আরuv sync --lockedদিয়ে ইনস্টল করুন। - Git LFS বড় ফাইলের বদলে আপনার Git হোস্টে পয়েন্টার বসায় (GitHub-এ ১০ GiB ফ্রি); DVC ডেটার ভার্সন রাখে আপনার নিজের স্টোরেজে এবং পাইপলাইন যোগ করে।
git log -S/-Gআরgit grep $(git rev-list --all)দিয়ে সিক্রেট খুঁজুন; আগে রোটেট করুন, তারপরgit filter-repoদিয়ে নতুন করে লিখুন, তারপর push protection আর pre-commit স্ক্যানিং দিয়ে ঠেকান।
এরপর: HTTP গভীরে: ইডেমপোটেন্সি, ক্যাশিং ও অথ রিপোজিটরি থেকে সরে আপনার AI সার্ভিসের এক্সপোজ করা API-তে যায়: কোন রিকোয়েস্ট নিরাপদে রিট্রাই করা যায়, ETag দিয়ে ক্যাশিং কীভাবে কাজ করে, আর OAuth আসলে ব্যবহারকারীকে কীভাবে লগইন করায়।