Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,22 @@ jobs:
# forgelm`` before any test runs.
run: |
pip install --upgrade pip
# Install TRL first (most restrictive), let pip resolve transitive deps
# transformers-5 migration floor (v0.9.0). Every gate is pinned
# EXPLICITLY so this job tests the true documented minimum — trl puts
# no upper bound on transformers, so an unpinned resolve would silently
# pull the newest 5.x and stop exercising the floor. transformers
# 5.3.0 is the CVE-2026-4372 fix floor and requires torch>=2.4 /
# huggingface_hub>=1.3 / tokenizers>=0.22; trl 1.0.0 pulls the
# accelerate>=1.4 and datasets>=4.7 floor. Keep 1:1 with the pyproject
# [project].dependencies minimums.
pip install \
"torch==2.3.0" \
"trl==0.12.0" \
"peft==0.11.0" \
"torch==2.4.0" \
"transformers==5.3.0" \
"huggingface_hub==1.3.0" \
"peft==0.19.0" \
"accelerate==1.4.0" \
"datasets==4.7.0" \
"trl==1.0.0" \
"pydantic==2.0.0" \
"pyyaml==6.0.1" \
"tensorboard==2.15.0" \
Expand Down
39 changes: 38 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,44 @@ All notable changes to ForgeLM are documented here.

## [Unreleased]

_(v0.8.1 dev cycle — entries will land here as PRs merge.)_
_(v0.9.0 dev cycle — a breaking dependency migration; entries land here as PRs merge.)_

### Changed

- **transformers 5.x migration (breaking).** Raised the core dependency floor to
`transformers>=5.3.0,<6.0.0` and cascaded the co-dependencies it requires:
`torch>=2.4.0`, `huggingface_hub>=1.3.0,<2.0.0`, `peft>=0.19.0`,
`accelerate>=1.4.0`, `datasets>=4.7.0,<6.0.0`, `trl>=1.0.0`, and
`requests>=2.32.2` (trl 1.0 pulls the `accelerate>=1.4` / `datasets>=4.7`
floor; datasets 4.7 pulls `requests>=2.32.2`). transformers 5.3.0 is the first
release carrying the fix for **CVE-2026-4372**; the previous `<5.0.0` pin could
not reach it. The `test-min-deps` nightly floor and `tools/pip_audit_ignores.yaml`
were updated to match the new minimums.
- **`from_pretrained` dtype kwarg.** Renamed `torch_dtype=` → `dtype=` at the two
base-model load sites (`export.py`, `synthetic.py`); `torch_dtype` is a
deprecated alias in transformers 5 slated for removal. Behaviour is unchanged.

### Removed

- **transformers 4.x support (breaking).** ForgeLM now requires transformers 5.x.
The `safe_serialization=True` kwarg (removed from `save_pretrained` in
transformers 5, where safetensors is the enforced default) was dropped from the
three model-save call sites (`export.py`, `merging.py`, `trainer.py`) — the
on-disk output is unchanged.
- **Intel Mac (x86_64) support (breaking).** transformers 5 requires `torch>=2.4`,
for which PyPI publishes no x86_64-Darwin wheel, so that platform can no longer
install ForgeLM's core stack. Apple Silicon, Linux, and Windows are unaffected.
The now-moot `numpy<2; darwin x86_64` ABI-guard marker was removed with it.

### Security

- **CVE-2026-4372** — a critical `AutoModelForCausalLM.from_pretrained()` RCE in
transformers <5.3.0 (a malicious `config.json` `_attn_implementation_internal`
field downloads and executes attacker code, bypassing `trust_remote_code`) — is
resolved by the `transformers>=5.3.0` floor above. Both transformers
suppressions in `tools/pip_audit_ignores.yaml` — `CVE-2026-1839` (fixed in
5.0.0rc3) and `PYSEC-2025-217` / `CVE-2025-14929` (X-CLIP RCE, OSV
last-affected 5.0.0rc0) — are now inert under the raised pin and were removed.

## [0.8.0] — 2026-06-16

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ cd ForgeLM
pip install -e .
```

**Prerequisites:** Python 3.10+, `torch>=2.2.0`. Platform-specific notes are in the [installation guide](docs/usermanuals/en/getting-started/installation.md).
**Prerequisites:** Python 3.10+, `torch>=2.4.0` (required by `transformers>=5.3.0`). Intel Macs (x86_64) are no longer supported — PyPI has no `torch>=2.4` wheel for that platform. Platform-specific notes are in the [installation guide](docs/usermanuals/en/getting-started/installation.md).

### Optional extras

Expand Down
7 changes: 4 additions & 3 deletions docs/reference/supply_chain_security-tr.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,10 @@ annotation olarak loglanır; audit trail görünür kalır.

ForgeLM'in kendi nightly'si proje-içi triage için check-in edilmiş
bir [`tools/pip_audit_ignores.yaml`](../../tools/pip_audit_ignores.yaml)
taşır (şu an **iki** transformers advisory'si (`CVE-2026-1839` ve
`PYSEC-2025-217` / `CVE-2025-14929`), dokuz no-fix torch advisory'si ve
bir OSV-yanlış-sınıflandırılmış markdown kaydı). Bu dosya yalnız projenin kendi workflow'u tarafından
taşır (şu an dokuz no-fix torch advisory'si ve bir
OSV-yanlış-sınıflandırılmış markdown kaydı; daha önce taşıdığı iki
transformers advisory'si `transformers>=5.3.0` floor'u onları pip-audit
tarafından raporlanamaz hâle getirince kaldırıldı). Bu dosya yalnız projenin kendi workflow'u tarafından
`--ignores` ile tüketilir; deployer'lar miras almaz. Her release
cycle'da gözden geçirilir.

Expand Down
7 changes: 4 additions & 3 deletions docs/reference/supply_chain_security.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,10 @@ the run summary so the audit trail stays visible.

ForgeLM's own nightly does carry a checked-in
[`tools/pip_audit_ignores.yaml`](../../tools/pip_audit_ignores.yaml) for
project-internal triage (currently **two** transformers advisories
(`CVE-2026-1839` and `PYSEC-2025-217` / `CVE-2025-14929`), nine
no-fix torch advisories, and one OSV-misclassified markdown record). That file is consumed only by the
project-internal triage (currently nine no-fix torch advisories and one
OSV-misclassified markdown record; the two transformers advisories it once
carried were dropped once the `transformers>=5.3.0` floor made them
unreportable by pip-audit). That file is consumed only by the
project's own workflow via `--ignores`; deployers do not inherit it.
Review at every release cycle.

Expand Down
4 changes: 4 additions & 0 deletions docs/usermanuals/en/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ ForgeLM ships as a single PyPI package with optional dependency groups (called *
**No GPU?** You can still use ForgeLM for ingestion, audit, evaluation prep, and deployment config generation — every CPU-only workflow runs on the same `forgelm` command.
:::

:::note
**Intel Macs (x86_64) are not supported.** ForgeLM requires `torch>=2.4.0` (pulled in by `transformers>=5.3.0`), and PyPI publishes no `torch>=2.4` wheel for x86_64 macOS. Use Apple Silicon, Linux, or Windows.
:::

## Base install

```shell
Expand Down
4 changes: 4 additions & 0 deletions docs/usermanuals/tr/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ ForgeLM, PyPI'da tek bir paket olarak gelir; ağır özellikler için opsiyonel
**GPU yok mu?** ForgeLM'i ingest, audit, eval hazırlığı ve deployment config üretimi için yine de kullanabilirsiniz — her CPU-only iş akışı aynı `forgelm` komutuyla çalışır.
:::

:::note
**Intel Mac'ler (x86_64) desteklenmiyor.** ForgeLM `torch>=2.4.0` gerektirir (`transformers>=5.3.0` üzerinden gelir) ve PyPI'da x86_64 macOS için `torch>=2.4` wheel'i yok. Apple Silicon, Linux veya Windows kullanın.
:::

## Temel kurulum

```shell
Expand Down
12 changes: 8 additions & 4 deletions forgelm/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,10 @@
def _merge_adapter(model_path: str, adapter_path: str, merged_dir: str) -> None:
"""Merge a PEFT LoRA adapter into the base model and save to *merged_dir*.

Forces ``torch_dtype=float16`` for the merge: GGUF f16 conversion will cast
Forces ``dtype=float16`` for the merge: GGUF f16 conversion will cast
to fp16 anyway, and bf16 math on CPU silently falls back to fp32 (much
slower for large models).
slower for large models). (``dtype`` is the transformers-5 name for the
former ``torch_dtype`` kwarg.)
"""
import torch
from peft import PeftModel
Expand All @@ -157,14 +158,17 @@
# model must not execute repo-bundled code. Operators with a custom
# architecture should fork and pre-convert before exporting.
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=False)
model = AutoModelForCausalLM.from_pretrained(
model_path, torch_dtype=torch.float16, device_map="cpu", trust_remote_code=False
model_path, dtype=torch.float16, device_map="cpu", trust_remote_code=False
)

Check warning on line 163 in forgelm/export.py

View check run for this annotation

codefactor.io / CodeFactor

forgelm/export.py#L161-L163

Unsafe Hugging Face Hub download without revision pinning in from_pretrained() (B615)
model = PeftModel.from_pretrained(model, adapter_path)
model = model.merge_and_unload()

os.makedirs(merged_dir, exist_ok=True)
model.save_pretrained(merged_dir, safe_serialization=True)
# safetensors is the enforced default in transformers 5.x, which removed the
# `safe_serialization` kwarg from save_pretrained entirely — passing it now
# raises TypeError. Dropping it preserves the previous safetensors output.
model.save_pretrained(merged_dir)
tokenizer.save_pretrained(merged_dir)
logger.info("Merged model saved to %s", merged_dir)

Expand Down
6 changes: 4 additions & 2 deletions forgelm/merging.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,11 @@ def merge_peft_adapters(
else:
return MergeResult(success=False, error=f"Unknown merge method: {method}")

# Save merged model
# Save merged model. transformers 5.x removed the `safe_serialization`
# kwarg (safetensors is now the enforced default); passing it raises
# TypeError, so we rely on the default.
os.makedirs(output_dir, exist_ok=True)
merged.save_pretrained(output_dir, safe_serialization=True)
merged.save_pretrained(output_dir)
tokenizer.save_pretrained(output_dir)
logger.info("Merged model saved to %s", output_dir)

Expand Down
3 changes: 2 additions & 1 deletion forgelm/synthetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,12 +276,13 @@
# an arbitrary teacher checkpoint. Operators that genuinely need a
# custom architecture should fork and pre-convert.
tokenizer = AutoTokenizer.from_pretrained(self.synth_cfg.teacher_model, trust_remote_code=False)
model = AutoModelForCausalLM.from_pretrained(
self.synth_cfg.teacher_model,
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
# ``dtype`` is the transformers-5 name for the former ``torch_dtype``.
dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
device_map="auto" if torch.cuda.is_available() else None,
trust_remote_code=False,
)

Check warning on line 285 in forgelm/synthetic.py

View check run for this annotation

codefactor.io / CodeFactor

forgelm/synthetic.py#L279-L285

Unsafe Hugging Face Hub download without revision pinning in from_pretrained() (B615)
self._teacher = (model, tokenizer)

def _load_file_responses(self) -> dict:
Expand Down
38 changes: 18 additions & 20 deletions forgelm/trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,35 +614,31 @@ def _get_training_args_for_type(self):

kwargs["packing"] = bool(getattr(self.config.training, "packing", False))
kwargs["dataset_text_field"] = "text"
# Sequence-length cap parameter was renamed in trl 0.13:
# trl 0.12.x: ``SFTConfig(max_seq_length=...)``
# trl 0.13+ / 1.x: ``SFTConfig(max_length=...)`` — old name removed
# ``pyproject.toml`` pins ``trl>=0.12.0,<2.0.0``, so detect at runtime.
# Hard-fail (don't warn-and-continue) if neither name is exposed:
# silently dropping ``model.max_length`` would let TRL pick its
# default and produce a model trained against an unintended
# context window — exactly the "no silent failures" rule in
# ``SFTConfig``'s sequence-length cap is ``max_length`` in trl 0.13+
# (the ``max_seq_length`` name was retired after trl 0.12.x).
# ``pyproject.toml`` pins ``trl>=1.0.0,<2.0.0``, so every supported
# trl exposes ``max_length``; we drive it directly but still verify
# it via the runtime signature and hard-fail if a future trl removes
# or hides the parameter. Silently dropping ``model.max_length``
# would let TRL pick its default and train against an unintended
# context window — the "no silent failures" rule in
# docs/standards/error-handling.md.
sft_params = inspect.signature(SFTConfig).parameters
if "max_length" in sft_params:
kwargs["max_length"] = self.config.model.max_length
elif "max_seq_length" in sft_params:
kwargs["max_seq_length"] = self.config.model.max_length
else:
if "max_length" not in sft_params:
# Probe the trl version off the already-loaded module rather
# than re-importing — ``from trl import SFTConfig`` above
# left trl in ``sys.modules`` and ``SFTConfig.__module__``
# gives us the canonical package name without a redundant
# ``import trl`` line.
# than re-importing — ``from trl import SFTConfig`` above left
# trl in ``sys.modules`` and ``SFTConfig.__module__`` gives the
# canonical package name without a redundant ``import trl``.
trl_module = sys.modules.get(SFTConfig.__module__.split(".", 1)[0])
trl_version = getattr(trl_module, "__version__", "?")
raise ValueError(
f"SFTConfig in trl {trl_version} exposes neither "
"`max_length` nor `max_seq_length` as a named parameter; cannot apply "
f"the sequence-length cap from config (model.max_length={self.config.model.max_length}). "
"`max_length` nor the legacy `max_seq_length` as a named parameter; "
f"cannot apply the sequence-length cap from config (model.max_length={self.config.model.max_length}). "
f"Detected parameters: {sorted(sft_params)}. Pin trl to a known-compatible "
"version or file an issue referencing this error."
)
kwargs["max_length"] = self.config.model.max_length
return SFTConfig(**kwargs)

elif tt == "orpo":
Expand Down Expand Up @@ -1525,7 +1521,9 @@ def save_final_model(self, final_path: str) -> None:
model_to_save = self.trainer.model
try:
merged = model_to_save.merge_and_unload()
merged.save_pretrained(final_path, safe_serialization=True)
# transformers 5.x removed the `safe_serialization` kwarg
# (safetensors is the enforced default); passing it raises TypeError.
merged.save_pretrained(final_path)
except (OSError, RuntimeError, AttributeError, ValueError) as e:
# AttributeError: non-PEFT model lacking merge_and_unload.
# RuntimeError: torch-side merge / dtype / device errors.
Expand Down
32 changes: 15 additions & 17 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "forgelm"
version = "0.8.1rc1"
version = "0.9.0rc1"
description = "Config-driven LLM fine-tuning with safety evaluation, EU AI Act compliance, 6 alignment methods, and one-command bundled quickstart templates."
readme = "README.md"
requires-python = ">=3.10"
Expand All @@ -39,25 +39,23 @@ classifiers = [
]

dependencies = [
"torch>=2.2.0,<3.0.0",
# Intel Mac (x86_64) is wheel-locked to torch 2.2.x — PyTorch Foundation
# stopped publishing >=2.3 wheels for x86_64 Darwin. torch 2.2 was
# compiled against NumPy 1.x; without this marker pip pulls numpy>=2 and
# triggers an ABI mismatch (`_ARRAY_API not found`) that silently
# degrades the numpy bridge inside torch. Other platforms keep numpy
# unconstrained — torch 2.3+ on Linux / Apple Silicon / Windows is
# binary-compatible with NumPy 2.x.
"numpy<2; sys_platform == 'darwin' and platform_machine == 'x86_64'",
"transformers>=4.38.0,<5.0.0",
"peft>=0.9.0,<1.0.0",
"datasets>=2.21.0,<5.0.0",
"accelerate>=0.27.0,<2.0.0",
"torch>=2.4.0,<3.0.0",
# transformers 5.x requires torch>=2.4. PyTorch Foundation never published
# >=2.3 wheels for x86_64 Darwin, so Intel Macs (x86_64) can no longer
# install ForgeLM's core dependency stack — that platform was dropped with
# the transformers-5 migration (v0.9.0). Apple Silicon, Linux and Windows
# are unaffected. The previous `numpy<2; darwin x86_64` ABI-guard marker is
# gone with the platform it protected (torch 2.4+ ships against NumPy 2.x).
"transformers>=5.3.0,<6.0.0",
"peft>=0.19.0,<1.0.0",
"datasets>=4.7.0,<6.0.0",
"accelerate>=1.4.0,<2.0.0",
"pyyaml>=6.0.1,<7.0.0",
"pydantic>=2.0.0,<3.0.0",
"trl>=0.12.0,<2.0.0",
"trl>=1.0.0,<2.0.0",
"tensorboard>=2.15.0,<3.0.0",
"huggingface_hub>=0.23.2,<1.0.0",
"requests>=2.31.0,<3.0.0",
"huggingface_hub>=1.3.0,<2.0.0",
"requests>=2.32.2,<3.0.0",
# requests-toolbelt is required by forgelm._http for the HostHeaderSSLAdapter
# used to close the DNS-rebinding TOCTOU window in safe_post / safe_get
# (issue #14). Small (~30KB) and from the same requests ecosystem, so it's
Expand Down
10 changes: 7 additions & 3 deletions tests/test_check_pip_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,11 @@ def test_project_ignore_file_passes_schema_validation(tool):
assert project_file.exists(), "project ignore file must be checked in"
loaded = tool._load_ignores(project_file)
assert loaded is not None, "checked-in ignore file failed schema validation"
# Every entry indexed under at least its primary id.
assert "CVE-2026-1839" in loaded, (
"the historical transformers ignore must still be present; if you removed it intentionally update this test"
# Every entry indexed under at least its primary id. (The transformers
# advisories that used to be asserted here — CVE-2026-1839 and
# PYSEC-2025-217 — were dropped in the v0.9.0 transformers-5 migration:
# pip-audit no longer reports either under the transformers>=5.3.0 pin.
# Assert on a still-present torch entry so the loader/indexing check holds.)
assert "PYSEC-2025-191" in loaded, (
"a known suppression must be indexed by its primary id; if you removed it intentionally update this test"
)
30 changes: 3 additions & 27 deletions tests/test_trainer_sft_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
name. Passing the old kwarg raises ``TypeError`` at trainer args build
time and aborts training on every modern trl install.

``pyproject.toml`` pins ``trl>=0.12.0,<2.0.0``, so the trainer must
detect at runtime which parameter the installed ``SFTConfig`` accepts.
``pyproject.toml`` pins ``trl>=1.0.0,<2.0.0`` (all expose ``max_length``),
so the trainer drives ``max_length`` directly but verifies it at runtime
and hard-fails if a future trl removes or hides the parameter.
"""

from __future__ import annotations
Expand Down Expand Up @@ -77,31 +78,6 @@ def __init__(self, *, max_length=None, packing=False, dataset_text_field=None, *
assert captured_kwargs["dataset_text_field"] == "text"


def test_sft_config_uses_max_seq_length_on_legacy_trl(tmp_path):
"""trl 0.12.x: ``SFTConfig`` accepts ``max_seq_length`` only. The
trainer must fall back to that name when the modern ``max_length``
parameter is unavailable."""

captured_kwargs: dict = {}

class FakeLegacySFTConfig:
def __init__(self, *, max_seq_length=None, packing=False, dataset_text_field=None, **other):
captured_kwargs["max_seq_length"] = max_seq_length
captured_kwargs["packing"] = packing
captured_kwargs["dataset_text_field"] = dataset_text_field
captured_kwargs.update(other)

trainer = _seed_trainer(tmp_path)

with patch("trl.SFTConfig", FakeLegacySFTConfig):
trainer._get_training_args_for_type()

assert captured_kwargs["max_seq_length"] == 2048
assert "max_length" not in captured_kwargs, (
"Modern `max_length` kwarg must NOT be passed to legacy trl 0.12.x SFTConfig."
)


def test_sft_config_passes_packing_and_dataset_text_field(tmp_path):
"""Regression: ``packing`` and ``dataset_text_field`` must keep being
propagated alongside the sequence-length cap."""
Expand Down
Loading
Loading