diff --git a/.gitignore b/.gitignore index d51a2c31..602abe78 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ site/ debug_code/ .pytest_cache/ *.pt +**/.env # Large test data (download separately if needed for regression tests) tests/onecomp/quantizer/jointq/data/model_layers_0_self_attn_k_proj.pth .uv-sync.lock diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8f076434..6b75917c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,6 +23,18 @@ variables: GIT_DEPTH: "1" PYTEST_MARKERS: "not slow" +lint:conflict-markers: + stage: lint + tags: + - lint + variables: + # The MR diff base may be outside the default shallow clone. + GIT_DEPTH: "0" + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + script: + - git diff --check "${CI_MERGE_REQUEST_DIFF_BASE_SHA}" "${CI_COMMIT_SHA}" + lint:format: extends: .skip_docs_only stage: lint diff --git a/CHANGELOG.md b/CHANGELOG.md index 556995d1..904685f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Change log +## [v1.3.2] 2026-08-24 + +### Bug Fix + +- Fix WikiText dataset loading in clean environments by using the canonical + `Salesforce/wikitext` dataset ID for perplexity evaluation and LoRA SFT examples. +- Fix warning regarding the data type arguments of Transformers. +- Fixed GemLite AssertionError during CPU inference execution for OneComp with Llama.cpp. +- Fixed `QuantizedModelLoader.load_quantized_model()` calling `unfuse_moe_experts()` twice on non-fused MoE checkpoints. An earlier unconditional unfuse (run right after building the empty model) was left in place when the checkpoint-aware unfuse (added for gpt-oss fused-MoE support) was introduced. The redundant unconditional call was removed, and the checkpoint-aware unfuse now runs before `_remap_state_dict_keys()` so key remapping still aligns against the unfused per-expert module paths. This also prevents wrongly unfusing gpt-oss fused-MoE checkpoints, whose fused 3D expert tensors must be loaded as-is (`quantized_model_loader.py`). + +### Documentation + +- Add troubleshooting information for running OneComp with Llama.cpp on macOS. + ## [v1.3.1] 2026-08-06 ### Bug Fix @@ -94,11 +108,11 @@ ### Security -- **Unsafe deserialization hardening (CWE-502)**: `QuantizedModelLoader.load_quantized_model_pt()` (alias `onecomp.load_quantized_model_pt()`) previously called `torch.load(model.pt, weights_only=False)` unconditionally, allowing arbitrary code execution when loading a malicious `.pt` checkpoint. It now refuses to load unless the caller explicitly opts in via `allow_unsafe_deserialization=True`, and emits a strong warning when it does load. For untrusted models, use the safetensors-based `load_quantized_model()`, which does not execute code. +- **Unsafe deserialization hardening (CVE-2026-73325, CWE-502)**: `QuantizedModelLoader.load_quantized_model_pt()` (alias `onecomp.load_quantized_model_pt()`) previously called `torch.load(model.pt, weights_only=False)` unconditionally, allowing arbitrary code execution when loading a malicious `.pt` checkpoint. It now refuses to load unless the caller explicitly opts in via `allow_unsafe_deserialization=True`, and emits a strong warning when it does load. For untrusted models, use the safetensors-based `load_quantized_model()`, which does not execute code. - **Breaking change**: existing callers of `load_quantized_model_pt()` must pass `allow_unsafe_deserialization=True` for trusted `.pt` files. - **`Quantizer.load_results()` / `ResultLoader`**: same hardening applied. Loading with `weights_only=False` now requires `allow_unsafe_deserialization=True` (added as a `ResultLoader` field), and logs a warning. The safe `weights_only=True` path is unchanged. - Updated docstrings, docs, and the LoRA SFT example to document the risk and the required opt-in. -- **Credit**: this unsafe deserialization issue (CWE-502) was responsibly disclosed by **Nir Yehoshua, Cipher Security Labs**. Thank you for the report. +- **Credit**: this unsafe deserialization issue (CVE-2026-73325, CWE-502) was responsibly disclosed by **Nir Yehoshua, Cipher Security Labs**. Thank you for the report. ## [v1.2.0] 2026-06-08 diff --git a/SECURITY.md b/SECURITY.md index 8d5ec4d7..b930d76a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,7 @@ Security updates are provided for the latest release line of Fujitsu One Compres | Version | Supported | | ------- | ------------------ | -| 1.2.2 | :white_check_mark: | +| 1.2.2 >= | :white_check_mark: | | < 1.2.2 | :x: | We recommend always upgrading to the latest release before reporting an issue. @@ -38,8 +38,20 @@ Please make a good-faith effort to avoid privacy violations, data destruction, a Thank you for helping keep OneComp and its users safe. +## Resolved Security Issues + +### CVE-2026-73325 — Unsafe Deserialization + +An unsafe deserialization vulnerability (CWE-502) was identified in +`QuantizedModelLoader.load_quantized_model_pt()`. + +- **Affected versions:** v1.2.0 and earlier +- **Fixed version:** v1.2.1 +- **CVE:** CVE-2026-73325 +- **Resolution:** The issue was addressed in v1.2.1. Users should upgrade to the latest supported release. + ## Security Acknowledgments We thank the following researchers for responsibly disclosing security issues in OneComp: -- **Nir Yehoshua, Cipher Security Labs** — unsafe deserialization in `QuantizedModelLoader.load_quantized_model_pt()` (CWE-502), fixed in v1.2.1. +- **Nir Yehoshua, Cipher Security Labs** — unsafe deserialization in `QuantizedModelLoader.load_quantized_model_pt()` (CVE-2026-73325, CWE-502), affecting OneComp versions through v1.2.0 and fixed in v1.2.1. diff --git a/docs/user-guide/cpu-inference.md b/docs/user-guide/cpu-inference.md index 860a32d9..ce2134a4 100644 --- a/docs/user-guide/cpu-inference.md +++ b/docs/user-guide/cpu-inference.md @@ -19,6 +19,59 @@ for inference. The direct export path additionally uses llama.cpp's pure-Python `convert_hf_to_gguf.py` to build the model metadata/tokenizer; it is fetched automatically (a shallow `git clone`) or taken from `$LLAMA_CPP_DIR` if set. + +### macOS + +#### Symptom + +If you have installed gcc or clang on macOS using a tool like Homebrew, the OpenMP dynamic library with them can conflict with the OpenMP dynamic library used by OneComp's PyTorch backend. + +- OneComp's PyTorch backend is often configured to use an OpenMP library located within the .venv directory. +- Depending on your environment, Llama.cpp is configured to use an OpenMP library associated with gcc or clang. + +If the following problems is occurring , this conflict may be occurring. + +- Your Python interpreter shows the following warning message. + +```python +.../multiprocessing/resource_tracker.py:279: UserWarning: resource_tracker: There appear to be 1 leaked semaphore objects to clean up at shutdown + warnings.warn('resource_tracker: There appear to be %d ' +``` + +- The following message is shown, when you import PyTorch and Llama.cpp in your script. + +```python +OMP: Error #15: Initializing libomp.dylib, but found libomp.dylib already initialized. +OMP: Hint This means that multiple copies of the OpenMP runtime have been linked into the program. +``` + +#### Solution + +##### 1. Search for OpenMP associated with PyTorch. + +Search for OpenMP library associated with PyTorch backend (using find command). + +```bash +$ find $PWD/.venv -type f \( -name 'libomp.dylib' -o -name 'libgomp*.dylib' \) -print | grep torch +/to/path/.venv/lib/python3./site-packages/torch/lib/libomp.dylib +``` + +##### 2. Create an .env + +Create a .env file and add the OpenMP path as an environment variable, as shown below. + +```bash +DYLD_LIBRARY_PATH="/to/path/.venv/lib/python3./site-packages/torch/lib" +``` + +##### 3. Run uv + +Pass the following `--env-file` options when running uv run. + +```bash +uv run --env-file /path/to/.env ... python your_script.py +``` + ## One entry point: `export_to_gguf` You do not need to know which path a checkpoint requires. `export_to_gguf` diff --git a/docs/user-guide/examples.md b/docs/user-guide/examples.md index fb988aed..cc7aee89 100644 --- a/docs/user-guide/examples.md +++ b/docs/user-guide/examples.md @@ -578,7 +578,7 @@ model_config = ModelConfig( gptq = GPTQ(wbits=4, groupsize=128) post_process = PostProcessLoraSFT( - dataset_name="wikitext", + dataset_name="Salesforce/wikitext", dataset_config_name="wikitext-2-raw-v1", train_split="train", text_column="text", diff --git a/docs/user-guide/post-process.md b/docs/user-guide/post-process.md index 45459a38..4dca6376 100644 --- a/docs/user-guide/post-process.md +++ b/docs/user-guide/post-process.md @@ -354,7 +354,7 @@ model_config = ModelConfig( gptq = GPTQ(wbits=4, groupsize=128) post_process = PostProcessLoraSFT( - dataset_name="wikitext", + dataset_name="Salesforce/wikitext", dataset_config_name="wikitext-2-raw-v1", train_split="train", text_column="text", @@ -512,7 +512,7 @@ model, tokenizer = load_quantized_model_pt( ```python PostProcessLoraSFT( - dataset_name="wikitext", + dataset_name="Salesforce/wikitext", dataset_config_name="wikitext-2-raw-v1", train_split="train", text_column="text", @@ -560,7 +560,7 @@ Teacher distillation aligns the quantized model's output distribution with a ful ```python post_process = PostProcessLoraSFT( - dataset_name="wikitext", + dataset_name="Salesforce/wikitext", dataset_config_name="wikitext-2-raw-v1", train_split="train", text_column="text", @@ -591,7 +591,7 @@ Intermediate block alignment adds a loss term that aligns hidden states at selec ```python post_process = PostProcessLoraSFT( - dataset_name="wikitext", + dataset_name="Salesforce/wikitext", dataset_config_name="wikitext-2-raw-v1", train_split="train", text_column="text", diff --git a/example/post_process/example_lora_sft.py b/example/post_process/example_lora_sft.py index 1bf962eb..03d0c7a4 100644 --- a/example/post_process/example_lora_sft.py +++ b/example/post_process/example_lora_sft.py @@ -66,7 +66,7 @@ def generate_text(model, tokenizer, prompt, device, max_new_tokens=64): gptq = GPTQ(wbits=4, groupsize=128) post_process = PostProcessLoraSFT( - dataset_name="wikitext", + dataset_name="Salesforce/wikitext", dataset_config_name="wikitext-2-raw-v1", train_split="train", text_column="text", diff --git a/notebook/01_tutorial.ipynb b/notebook/01_tutorial.ipynb index d55c887e..4f824b99 100644 --- a/notebook/01_tutorial.ipynb +++ b/notebook/01_tutorial.ipynb @@ -76,7 +76,7 @@ "DEVICE = \"cuda:0\" if torch.cuda.is_available() else \"cpu\"\n", "\n", "tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)\n", - "model = AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtype=torch.float16).to(DEVICE)\n", + "model = AutoModelForCausalLM.from_pretrained(MODEL_ID, dtype=torch.float16).to(DEVICE)\n", "model.eval()\n", "\n", "n_params = sum(p.numel() for p in model.parameters())\n", diff --git a/onecomp/__version__.py b/onecomp/__version__.py index f21c7223..0919865f 100644 --- a/onecomp/__version__.py +++ b/onecomp/__version__.py @@ -6,4 +6,4 @@ """ -__version__ = "1.3.1" +__version__ = "1.3.2" diff --git a/onecomp/cpu/export/dequantize.py b/onecomp/cpu/export/dequantize.py index 6a47ea4b..5493c9bd 100644 --- a/onecomp/cpu/export/dequantize.py +++ b/onecomp/cpu/export/dequantize.py @@ -98,7 +98,7 @@ def dequantize_to_hf( pass logger.info("Building empty dense model from config (%s)", config.model_type) - model = AutoModelForCausalLM.from_config(config, torch_dtype=torch_dtype) + model = AutoModelForCausalLM.from_config(config, dtype=torch_dtype) state: Dict[str, torch.Tensor] = {} for shard in sorted(glob(os.path.join(save_directory, "*.safetensors"))): diff --git a/onecomp/pre_process/prepare_rotated_model.py b/onecomp/pre_process/prepare_rotated_model.py index 8d0b74c8..50c05b06 100644 --- a/onecomp/pre_process/prepare_rotated_model.py +++ b/onecomp/pre_process/prepare_rotated_model.py @@ -319,7 +319,7 @@ def prepare_rotated_model( t0 = time.time() model = AutoModelForCausalLM.from_pretrained( model_path, - torch_dtype="auto", + dtype="auto", device_map="cpu", low_cpu_mem_usage=True, ) diff --git a/onecomp/pre_process/train_rotation.py b/onecomp/pre_process/train_rotation.py index 4bfacfdf..e3f0b32d 100644 --- a/onecomp/pre_process/train_rotation.py +++ b/onecomp/pre_process/train_rotation.py @@ -321,7 +321,7 @@ def _convert_model_structure(config, model_type, model_path, use_sdpa=False): if is_llama: quant_model = QLlamaFC.from_pretrained( model_path, - torch_dtype="auto", + dtype="auto", device_map="cpu", low_cpu_mem_usage=True, config=config, @@ -329,7 +329,7 @@ def _convert_model_structure(config, model_type, model_path, use_sdpa=False): elif is_qwen3: quant_model = QQwen3FC.from_pretrained( model_path, - torch_dtype="auto", + dtype="auto", device_map="cpu", low_cpu_mem_usage=True, config=config, diff --git a/onecomp/quantized_model_loader.py b/onecomp/quantized_model_loader.py index 33e65c14..2c450248 100644 --- a/onecomp/quantized_model_loader.py +++ b/onecomp/quantized_model_loader.py @@ -101,22 +101,14 @@ def load_quantized_model( torch_dtype = torch.bfloat16 model = cls._build_empty_model_from_config(config_dict, torch_dtype) - # Mirror the unfuse step performed before quantization/save (see - # Runner.save_quantized_model) so per-expert module names such as - # "model.layers.0.mlp.experts.0.down_proj" resolve against the - # freshly-built model instead of its fused 3D expert parameters. - if unfuse_moe_experts(model, logger): - logger.info("Unfused MoE expert tensors for quantized model load") - # Load state_dict from safetensors state_dict = cls._load_state_dict_from_dir(save_directory) - # Align checkpoint key prefixes with the empty model built from config. - # Gemma3 VLMs are a common case: weights saved from from_pretrained - # use model.language_model.model.layers. (language_model is a - # ForCausalLM wrapper) while from_config exposes - # model.language_model.layers.* directly. - state_dict = cls._remap_state_dict_keys(state_dict, model) + # Decide, from the checkpoint alone, whether the empty model must be + # unfused into per-expert nn.Linear modules. This must happen before + # _remap_state_dict_keys so remapping aligns checkpoint keys against the + # unfused module paths; fused-MoE checkpoints (e.g. gpt-oss) keep the + # fused 3D parameters and skip unfuse. from .utils.unfuse_moe import ( _checkpoint_uses_fused_moe, _expand_deduped_moe_keys, @@ -131,6 +123,13 @@ def load_quantized_model( elif unfuse_moe_experts(model, logger): logger.info("Unfused MoE expert tensors for quantized model load") + # Align checkpoint key prefixes with the empty model built from config. + # Gemma3 VLMs are a common case: weights saved from from_pretrained + # use model.language_model.model.layers. (language_model is a + # ForCausalLM wrapper) while from_config exposes + # model.language_model.layers.* directly. + state_dict = cls._remap_state_dict_keys(state_dict, model) + # Replace quantized layers with empty modules and align quantized # tensor keys with the actual module names in the model built from # config. This is required when the saved checkpoint and the @@ -568,11 +567,11 @@ def _build_empty_model_from_config( config_cls = CONFIG_MAPPING[model_type] model_config = config_cls.from_dict(clean_config) try: - return AutoModelForCausalLM.from_config(model_config, torch_dtype=dtype) + return AutoModelForCausalLM.from_config(model_config, dtype=dtype) except (ValueError, KeyError): from transformers import AutoModelForImageTextToText - return AutoModelForImageTextToText.from_config(model_config, torch_dtype=dtype) + return AutoModelForImageTextToText.from_config(model_config, dtype=dtype) @staticmethod def _set_module_by_name( diff --git a/onecomp/quantizer/gemlite.py b/onecomp/quantizer/gemlite.py index e50b5114..4e79b487 100644 --- a/onecomp/quantizer/gemlite.py +++ b/onecomp/quantizer/gemlite.py @@ -16,13 +16,15 @@ import torch.nn.functional as F # Optional GemLite/HQQ imports -try: - from gemlite.core import DType, GemLiteLinearTriton - from hqq.core.quantize import BaseQuantizeConfig, HQQLinear +HAS_GEMLITE = False +if torch.cuda.is_available(): + try: + from gemlite.core import DType, GemLiteLinearTriton + from hqq.core.quantize import BaseQuantizeConfig, HQQLinear - HAS_GEMLITE = True -except (ImportError, AttributeError): - HAS_GEMLITE = False + HAS_GEMLITE = True + except (ImportError, AttributeError, AssertionError): + pass # Constants diff --git a/onecomp/runner.py b/onecomp/runner.py index b869ef3a..2ffa41d8 100644 --- a/onecomp/runner.py +++ b/onecomp/runner.py @@ -1260,7 +1260,7 @@ def calculate_perplexity( original_model=False, dequantized_model=False, quantized_model=True, - dataset_name="wikitext", + dataset_name="Salesforce/wikitext", dataset_config="wikitext-2-raw-v1", split="test", max_samples=None, @@ -1342,7 +1342,7 @@ def benchmark_perplexity( original_model=True, dequantized_model=False, quantized_model=True, - dataset_name="wikitext", + dataset_name="Salesforce/wikitext", dataset_config="wikitext-2-raw-v1", split="test", max_samples=None, diff --git a/onecomp/utils/perplexity.py b/onecomp/utils/perplexity.py index 8c8742b3..d26c701f 100644 --- a/onecomp/utils/perplexity.py +++ b/onecomp/utils/perplexity.py @@ -41,7 +41,7 @@ def calculate_perplexity( model=None, tokenizer=None, model_config=None, - dataset_name="wikitext", + dataset_name="Salesforce/wikitext", dataset_config="wikitext-2-raw-v1", split="test", max_samples=None, @@ -53,7 +53,7 @@ def calculate_perplexity( Based on https://huggingface.co/docs/transformers/perplexity Args: - dataset_name (str): Dataset name (e.g. "wikitext", "allenai/c4"). + dataset_name (str): Dataset name (e.g. "Salesforce/wikitext", "allenai/c4"). dataset_config (str): Dataset configuration. - For WikiText: "wikitext-2-raw-v1" - For C4: "en/c4-train.00001-of-01024.json.gz" (treated as data_files) diff --git a/onecomp/utils/vram_estimator.py b/onecomp/utils/vram_estimator.py index 7320c38e..d38fbfb0 100644 --- a/onecomp/utils/vram_estimator.py +++ b/onecomp/utils/vram_estimator.py @@ -312,7 +312,7 @@ def estimate_wbits_from_vram( config = AutoConfig.from_pretrained(model_id) with torch.device("meta"): - model = AutoModelForCausalLM.from_config(config, torch_dtype=torch.float16) + model = AutoModelForCausalLM.from_config(config, dtype=torch.float16) return estimate_target_bitwidth( model, diff --git a/tests/onecomp/pre_process/test_save_load_pipeline_qwen3.py b/tests/onecomp/pre_process/test_save_load_pipeline_qwen3.py index abd7f8be..bfc52648 100644 --- a/tests/onecomp/pre_process/test_save_load_pipeline_qwen3.py +++ b/tests/onecomp/pre_process/test_save_load_pipeline_qwen3.py @@ -89,7 +89,7 @@ def test_save_load(self, model_id, quant_type, save_type, tmp_path): else: model_before = AutoModelForCausalLM.from_pretrained( rotated_config.path, - torch_dtype=torch.float16, + dtype=torch.float16, device_map="cpu", ) runner.update_model_weights(model_before) @@ -111,7 +111,7 @@ def test_save_load(self, model_id, quant_type, save_type, tmp_path): else: model = AutoModelForCausalLM.from_pretrained( save_dir, - torch_dtype=torch.float16, + dtype=torch.float16, device_map=device, ) tokenizer = AutoTokenizer.from_pretrained(save_dir) diff --git a/tests/onecomp/pre_process/test_save_load_pipeline_tinyllama.py b/tests/onecomp/pre_process/test_save_load_pipeline_tinyllama.py index 61d3d6a9..f19aa367 100644 --- a/tests/onecomp/pre_process/test_save_load_pipeline_tinyllama.py +++ b/tests/onecomp/pre_process/test_save_load_pipeline_tinyllama.py @@ -89,7 +89,7 @@ def test_save_load(self, model_id, quant_type, save_type, tmp_path): else: model_before = AutoModelForCausalLM.from_pretrained( rotated_config.path, - torch_dtype=torch.float16, + dtype=torch.float16, device_map="cpu", ) runner.update_model_weights(model_before) @@ -111,7 +111,7 @@ def test_save_load(self, model_id, quant_type, save_type, tmp_path): else: model = AutoModelForCausalLM.from_pretrained( save_dir, - torch_dtype=torch.float16, + dtype=torch.float16, device_map=device, ) tokenizer = AutoTokenizer.from_pretrained(save_dir) diff --git a/tests/onecomp/test_cli.py b/tests/onecomp/test_cli.py index 9676d64e..9b21465a 100644 --- a/tests/onecomp/test_cli.py +++ b/tests/onecomp/test_cli.py @@ -23,7 +23,7 @@ MODEL_ID = "TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T" -TIMEOUT = 1200 +TIMEOUT = 3600 # 1 hour _skip_variant = pytest.mark.skipif( not os.environ.get("RUN_CLI_VARIANT_TESTS"), diff --git a/tests/onecomp/test_quantized_model_loader_moe_unfuse.py b/tests/onecomp/test_quantized_model_loader_moe_unfuse.py index b2c84adf..871da6df 100644 --- a/tests/onecomp/test_quantized_model_loader_moe_unfuse.py +++ b/tests/onecomp/test_quantized_model_loader_moe_unfuse.py @@ -21,7 +21,9 @@ def test_unfuse_moe_experts_runs_before_state_dict_is_loaded(self, tmp_path): # only resolve if the empty model's fused gate_up_proj/down_proj # parameters have already been unfused into per-expert nn.Linear # modules, so unfuse_moe_experts must run before the state_dict is - # materialized against the model. + # materialized against the model. The unfuse decision is made after the + # checkpoint is read from disk (so fused-MoE checkpoints can skip it), + # but still before model.load_state_dict materializes the tensors. fake_model = MagicMock(name="empty_model") call_order = [] @@ -66,7 +68,7 @@ def test_unfuse_moe_experts_runs_before_state_dict_is_loaded(self, tmp_path): mock_unfuse.assert_called_once() assert mock_unfuse.call_args[0][0] is fake_model - assert call_order == ["build", "unfuse", "load_state_dict"] + assert call_order == ["build", "load_state_dict", "unfuse"] class _FakeMoEExpertsBlock(nn.Module):