Repository navigation
Conversation
|
Thanks for opening this PR. In the future, please open an issue first so that we can discuss the change beforehand. As to the problem, I don't have a Windows machine to test this. However, we have Windows on CI, so could you please add a unit test for this behavior? It should fail without the fix and pass with the fix. Guard the test on the platform being Windows. Also, as mentioned on #3772, please add that change here. |
|
Thanks for the review! Done both:
And noted re: opening an issue first next time — will do. |
There was a problem hiding this comment.
🤗 Serge says:
Verdict: The fix is correct, minimal, and safe — pinning encoding="utf-8" on both the read and write of adapter_config.json is right, and the shadow/model.py change is consistent with it. But the test only exercises the fix on Windows, and the PR does not meet the repo's agent-contribution/approval requirements.
Correctness
- Backwards-compatible:
save_pretrainedhas always written viajson.dumpswith defaultensure_ascii=True, so every config PEFT wrote is pure ASCII and decodes identically under UTF-8; non-UTF-8 legacy configs could only come from third-party tools, which is exactly the case this fixes. Verified these are the onlyopen()/json.loadtext-decode sites insrc/peft(the rest aresafe_openbinary readers).
Tests
test_from_json_file_utf8_non_asciiis gated onsys.platform.startswith("win"), so it never runs on the Ubuntu CI job. Onwindows-latestthe ANSI code page is cp1252 (not cp936), so it catches the bug there only incidentally and on one matrix leg; the same crash occurs on Linux under a C/POSIX locale, so the guard is unnecessarily narrow — see inline suggestion.- Only the read path is covered; a roundtrip through
save_pretrainedwith a non-ASCIIbase_model_name_or_pathwould also lock in the write-side pin.
serge v0.1.0 · model: zai-org/GLM-5.3-Flash · 14 LLM turns · 15 tool calls · 110.9s · 126641 in / 6875 out tokens
|
@dltsum Thanks for the update. I checked if the new test would fail in the Windows CI without the fix and it indeed does: https://github.com/huggingface/peft/actions/runs/35717602918/job/106713214870?pr=3785 I also asked the Serge bot to review the PR (using GLM 5.3 Flash). As mentioned, I don't have a Windows machine to verify this, but please check if the feedback makes sense. For the testing, there should also be a test specific to ShadowPEFT, right? |
|
Thanks for confirming the test goes red on Windows CI without the fix, and for the Serge review — both points made sense and are now addressed in a4cdcd7:
Both tests pass locally on a real Windows machine (cp936 locale). |
|
@BenjaminBossan Thanks for verifying the test on the Windows CI! The Serge bot feedback makes sense — addressed both points in a4cdcd7:
Verified red/green locally on a cp936 Windows machine: without the pins both tests fail with |
BenjaminBossan
left a comment
There was a problem hiding this comment.
Thanks for the updates. I still have some comments, please check.
| # Config files are UTF-8; without an explicit encoding, reading them falls back to the | ||
| # locale encoding (e.g. cp936 on Windows, or LC_ALL=C on Linux), crashing or corrupting | ||
| # literal non-ASCII values (e.g. a hand-edited config or one written by another tool with | ||
| # ensure_ascii=False). Not Windows-gated: the regression shows on any non-UTF-8 locale. |
There was a problem hiding this comment.
| # ensure_ascii=False). Not Windows-gated: the regression shows on any non-UTF-8 locale. | |
| # ensure_ascii=False). |
This extra comment doesn't make sense outside the context of the review of this PR. Only add comments that make sense on their own.
There was a problem hiding this comment.
Done in b326332 — replaced with a self-contained comment.
| def test_from_json_file_utf8_non_ascii(self, tmp_path, monkeypatch): | ||
| # Simulate a non-UTF-8 locale (e.g. cp1252/cp936 on Windows, LC_ALL=C on POSIX): any | ||
| # text-mode open() without an explicit encoding decodes as cp1252 instead of UTF-8. | ||
| # (Patching locale.getpreferredencoding is not enough: the C io layer caches the |
There was a problem hiding this comment.
Is it not possible to call locale.setlocale?
There was a problem hiding this comment.
Good question — I tried that first, but setlocale can't reliably produce a legacy-encoding locale inside an already-running Python process:
- Windows:
locale.setlocale(locale.LC_ALL, "C")doesn't change whatopen()uses —locale.getpreferredencoding(False)still returns the ANSI code page (e.g. cp1252/cp936) afterwards, because that value comes from the OS code page, not the C locale. Verified locally on Windows 11 / Python 3.14: before and aftersetlocale(LC_ALL, "C")the preferred encoding is unchanged. - POSIX: with
LC_ALL=C, CPython's UTF-8 mode / PEP 538 coercion usually makes the locale encoding report UTF-8 anyway, so the test would silently stop exercising the legacy-decode path on Linux CI.
The goal of the monkeypatch is just to make the default-encoding path deterministic on every platform (fail without the fix, pass with it), independent of the host locale. Happy to switch if you know a cleaner way to force a non-UTF-8 default encoding mid-process.
There was a problem hiding this comment.
Hmm, that's too bad. I think this all makes the testing more complicated than it's worth it. I'd say, let's go back to the status from before you where you only checked Windows, skipping the test on other platforms.
There was a problem hiding this comment.
Agreed — done in f44c15e. Back to the Windows-gated version: skip on non-Windows, plain write-then-read with literal non-ASCII, and the save_pretrained roundtrip from the last round kept. Ran it on a real Windows machine (cp936 locale): passes with the pin, and this is the scenario that fails on Windows CI without it.
BenjaminBossan
left a comment
There was a problem hiding this comment.
Thanks for the updates. I still have some comments.
To verify that the tests fail on Windows without the fix, I started a CI run with those tests (#3970). It looks like the ShadowPEFT passes even without the fix, could you please check why?
Please also run make style before pushing your changes.
| uncached = model.generate(input_ids=ids, max_new_tokens=4, use_cache=False, do_sample=False) | ||
| assert torch.equal(cached, uncached) | ||
|
|
||
| def test_explicit_shadow_model_utf8_config(self, tmp_path): |
There was a problem hiding this comment.
This should also be skipped on Platforms other than Windows.
There was a problem hiding this comment.
Done in 432f61c — now skipped unless platform.system() == "Windows", same as the test_config.py one.
| # Regression for the UTF-8 pin in `_load_shadow_backbone`: a shadow checkpoint whose | ||
| # config.json contains literal non-ASCII (e.g. a CJK `_name_or_path`, hand-edited or written | ||
| # by another tool with ensure_ascii=False) must load on locales whose default encoding is | ||
| # not UTF-8 (e.g. cp936 on Windows, or LC_ALL=C on Linux). |
There was a problem hiding this comment.
Comment needs updating regarding Linux. Also, please refer to the other test and explain why we need this specific one for ShadowPEFT.
There was a problem hiding this comment.
Done in 432f61c — comment rewritten: dropped the Linux reference, and it now points to test_from_json_file_utf8_non_ascii in tests/test_config.py, explaining that the shadow backbone config loads through a separate code path (_load_shadow_backbone) so it needs its own regression. Verified on a real Windows machine (cp936): passes.
|
@dltsum There is a merge conflict, could you please check? |
`PeftConfig.from_json_file` read the config with the locale's preferred encoding. On Windows with a non-UTF-8 locale (e.g. cp936), any adapter config containing raw non-ASCII JSON (for example a CJK path stored in `base_model_name_or_path` by a hand-edited or third-party-written config) crashes with `UnicodeDecodeError`. Pin `encoding="utf-8"` on the read, and on the write in `save_pretrained` for symmetry (the write was already ASCII-safe via `json.dumps` defaults, this only makes the encoding explicit). Co-Authored-By: Claude Code <noreply@anthropic.com>
As requested in review: - Pin UTF-8 on the shadow backbone's config.json read as well (folded in from huggingface#3772, which will be closed). A repo-wide scan found no other user-facing config reads missing the pin. - Add a Windows-gated regression test: a config file containing literal UTF-8 non-ASCII values (as written by other tools or by hand) used to crash or decode incorrectly under a non-UTF-8 locale (e.g. cp936). The test fails without the fix and passes with it. Co-Authored-By: Claude Code <noreply@anthropic.com>
… test - test_from_json_file_utf8_non_ascii: drop the Windows-only skip (the regression shows on any non-UTF-8 locale, e.g. LC_ALL=C on Linux) and add a save_pretrained roundtrip to lock in the write-side pin. - Add a ShadowPEFT-specific regression test: a shadow checkpoint whose config.json carries literal non-ASCII (_name_or_path) must load under non-UTF-8 locales. Co-Authored-By: Claude Code <noreply@anthropic.com>
- Simulate a non-UTF-8 locale by patching builtins.open so text-mode opens without an explicit encoding decode as cp1252 (patching locale.getpreferredencoding is ineffective: the C io layer uses the startup locale encoding). The test now fails without the encoding pin on Linux as well, not just on cp936 Windows. - Replace the review-context comment with a self-contained one.
Simplify per review: drop the builtins.open simulation and go back to the Windows-only regression test (the locale-encoding fallback only manifests there; the monkeypatch simulation added more complexity than value). The save_pretrained roundtrip from the previous round is kept so the write-side pin stays locked in.
432f61c to
870350a
Compare
|
@BenjaminBossan Rebased onto current main and resolved the conflict — upstream switched to |
Problem
PeftConfig.from_json_filereadsadapter_config.jsonusing the locale's preferred encoding:On Windows with a non-UTF-8 locale (e.g. cp936/GBK), loading an adapter whose config contains raw non-ASCII JSON crashes:
A realistic source of such configs: a user (or a third-party tool) writes
base_model_name_or_pathpointing at a local model directory with CJK characters, saved as UTF-8 without ASCII escaping.Reproducer (Windows 11, cp936, Python 3.14)
Fix
Pin
encoding="utf-8"on the read, and on the corresponding write insave_pretrainedfor symmetry (the write was already ASCII-safe throughjson.dumpsdefaults; this only makes the encoding explicit).Verification
UnicodeDecodeError; after: parses correctly.pytest tests/test_config.py→ 1282 passed, 12 skipped.🤖 Generated with Claude Code