Skip to content

Commit 6139332

Browse files
rootroot
authored andcommitted
fixing PR comments
1 parent 2ca3e68 commit 6139332

7 files changed

Lines changed: 287 additions & 48 deletions

File tree

superbench/benchmarks/micro_benchmarks/huggingface_model_loader.py

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""Hugging Face model loader for benchmarking."""
55

66
import os
7+
import re
78
from pathlib import Path
89
from typing import Optional, Tuple
910

@@ -20,6 +21,39 @@
2021
from superbench.common.utils import logger
2122
from superbench.benchmarks.micro_benchmarks.model_source_config import ModelSourceConfig
2223

24+
# Strict allow-list for HuggingFace model identifiers. Accepts either a bare
25+
# repo name ('bert-base-uncased') or 'namespace/name' form, restricted to the
26+
# character set HF itself uses and bounded in length. Rejects '..', backslash,
27+
# colon, control chars, absolute paths, and anything that could be interpreted
28+
# as a local filesystem path by AutoConfig.from_pretrained (which silently
29+
# loads from disk when given a path that exists).
30+
_SAFE_MODEL_ID_RE = re.compile(r'^[A-Za-z0-9][A-Za-z0-9._-]{0,127}(/[A-Za-z0-9._-]{1,128})?$')
31+
32+
33+
def validate_model_identifier(model_identifier: Optional[str]) -> str:
34+
"""Validate a HuggingFace model identifier against a strict allow-list.
35+
36+
Args:
37+
model_identifier: The identifier to validate (typically from CLI input).
38+
39+
Returns:
40+
The validated identifier (unchanged) for convenient inline use.
41+
42+
Raises:
43+
ValueError: If the identifier is missing or does not match the
44+
permitted ``[namespace/]name`` shape. The check intentionally
45+
rejects path-traversal sequences and characters that could let
46+
``from_pretrained`` load attacker-staged files from disk.
47+
"""
48+
if not model_identifier or not _SAFE_MODEL_ID_RE.match(model_identifier):
49+
raise ValueError(
50+
f'Invalid model_identifier {model_identifier!r}. '
51+
'Must be a HuggingFace repo id matching '
52+
"'^[A-Za-z0-9][A-Za-z0-9._-]{0,127}(/[A-Za-z0-9._-]{1,128})?$' "
53+
'(e.g. "bert-base-uncased" or "meta-llama/Llama-2-7b-hf").'
54+
)
55+
return model_identifier
56+
2357

2458
class ModelLoadError(Exception):
2559
"""Exception raised when model loading fails."""
@@ -46,23 +80,41 @@ class HuggingFaceModelLoader:
4680
Attributes:
4781
cache_dir: Directory to cache downloaded models.
4882
token: HuggingFace authentication token for private/gated models.
83+
allow_remote_code: Whether to allow HuggingFace to download and execute
84+
repository-provided Python (``trust_remote_code=True``). Default
85+
``False``; enabling this turns ``--model_identifier`` into an RCE
86+
sink, so it is opt-in only.
4987
"""
50-
def __init__(self, cache_dir: Optional[str] = None, token: Optional[str] = None):
88+
def __init__(
89+
self,
90+
cache_dir: Optional[str] = None,
91+
token: Optional[str] = None,
92+
allow_remote_code: bool = False,
93+
):
5194
"""Initialize the HuggingFace model loader.
5295
5396
Args:
5497
cache_dir: Directory to cache downloaded models. If None, uses HF default.
5598
token: HuggingFace authentication token for private/gated models.
99+
allow_remote_code: If True, allow execution of model-repo Python via
100+
``trust_remote_code=True``. Default False. Only enable for
101+
trusted ``--model_identifier`` values; pin ``--revision <sha>``.
56102
"""
57103
self.cache_dir = cache_dir or os.getenv('HF_HOME') or os.path.expanduser('~/.cache/huggingface')
58104
self.token = token or os.getenv('HF_TOKEN') or os.getenv('HUGGING_FACE_HUB_TOKEN')
105+
self.allow_remote_code = bool(allow_remote_code)
59106

60107
# Ensure cache directory exists
61108
Path(self.cache_dir).mkdir(parents=True, exist_ok=True)
62109

63110
logger.info(f'HuggingFaceModelLoader initialized with cache_dir: {self.cache_dir}')
64111
if self.token:
65112
logger.info('Authentication token provided for private/gated models (token not logged)')
113+
if self.allow_remote_code:
114+
logger.warning(
115+
'allow_remote_code=True: HuggingFace may download and execute arbitrary Python '
116+
'from model repositories. Only enable for trusted model identifiers; pin --revision.'
117+
)
66118

67119
def load_model(
68120
self,
@@ -94,6 +146,9 @@ def load_model(
94146
"""
95147
logger.info(f'Loading model: {model_identifier}')
96148

149+
# Reject malformed / path-like identifiers before any network or disk activity.
150+
validate_model_identifier(model_identifier)
151+
97152
try:
98153
# Convert torch_dtype string to torch dtype
99154
dtype = self._get_torch_dtype(torch_dtype) if torch_dtype else None
@@ -112,22 +167,26 @@ def load_model(
112167
# Load config (use pre-downloaded config if provided)
113168
if config is None:
114169
logger.info('Loading model configuration...')
115-
config = AutoConfig.from_pretrained(model_identifier, trust_remote_code=True, **load_kwargs)
170+
config = AutoConfig.from_pretrained(
171+
model_identifier, trust_remote_code=self.allow_remote_code, **load_kwargs
172+
)
116173
else:
117174
logger.info('Using pre-downloaded model configuration.')
118175

119176
# Load tokenizer (may fail for some models, that's ok)
120177
tokenizer = None
121178
try:
122179
logger.info('Loading tokenizer...')
123-
tokenizer = AutoTokenizer.from_pretrained(model_identifier, trust_remote_code=True, **load_kwargs)
180+
tokenizer = AutoTokenizer.from_pretrained(
181+
model_identifier, trust_remote_code=self.allow_remote_code, **load_kwargs
182+
)
124183
except Exception as e:
125184
logger.warning(f'Could not load tokenizer: {e}. Continuing without tokenizer.')
126185

127186
# Load model
128187
logger.info(f'Loading model weights (dtype={torch_dtype}, device={device})...')
129188
model_kwargs = load_kwargs.copy()
130-
model_kwargs['trust_remote_code'] = True
189+
model_kwargs['trust_remote_code'] = self.allow_remote_code
131190

132191
# Handle device mapping for large models
133192
effective_device_map = device_map
@@ -202,16 +261,31 @@ def load_model_from_config(
202261
if device is None:
203262
device = 'cuda' if torch.cuda.is_available() else 'cpu'
204263

205-
# Extract loading parameters
206-
return self.load_model(
207-
model_identifier=config.identifier,
208-
torch_dtype=config.torch_dtype,
209-
device=device,
210-
revision=config.revision,
211-
device_map=config.device_map,
212-
config=config_pretrained,
213-
**config.additional_kwargs
214-
)
264+
# Honor explicit per-call hf_token / cache_dir from the config without permanently
265+
# mutating the loader instance. This makes ModelSourceConfig the single source of
266+
# truth for callers that don't rely on HF_TOKEN / HF_HOME env vars.
267+
original_token = self.token
268+
original_cache_dir = self.cache_dir
269+
try:
270+
if config.hf_token:
271+
self.token = config.hf_token
272+
if config.cache_dir:
273+
self.cache_dir = config.cache_dir
274+
Path(self.cache_dir).mkdir(parents=True, exist_ok=True)
275+
276+
# Extract loading parameters
277+
return self.load_model(
278+
model_identifier=config.identifier,
279+
torch_dtype=config.torch_dtype,
280+
device=device,
281+
revision=config.revision,
282+
device_map=config.device_map,
283+
config=config_pretrained,
284+
**config.additional_kwargs
285+
)
286+
finally:
287+
self.token = original_token
288+
self.cache_dir = original_cache_dir
215289

216290
def _get_torch_dtype(self, dtype_str: str) -> torch.dtype:
217291
"""Convert dtype string to torch.dtype.
@@ -242,7 +316,7 @@ def _get_torch_dtype(self, dtype_str: str) -> torch.dtype:
242316
}
243317

244318
if normalized_dtype not in dtype_map:
245-
raise ValueError(f"Invalid dtype '{dtype_str}'.Must be one of {list(dtype_map.keys())}")
319+
raise ValueError(f"Invalid dtype '{dtype_str}'. Must be one of {list(dtype_map.keys())}")
246320

247321
return dtype_map[normalized_dtype]
248322

superbench/benchmarks/micro_benchmarks/model_source_config.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ def __post_init__(self):
4747
if self.source not in ['in-house', 'huggingface']:
4848
raise ValueError(f"Invalid model source '{self.source}'. Must be 'in-house' or 'huggingface'.")
4949

50-
# Validate torch_dtype
51-
valid_dtypes = ['float32', 'float16', 'bfloat16', 'int8']
50+
# Validate torch_dtype. NOTE: 'int8' is intentionally excluded here — it is handled
51+
# post-export via quantize_dynamic (see ort_inference_performance.py) rather than via
52+
# the HF torch_dtype loading path, which does not accept torch.int8.
53+
valid_dtypes = ['float32', 'float16', 'bfloat16']
5254
if self.torch_dtype not in valid_dtypes:
5355
raise ValueError(f"Invalid torch_dtype '{self.torch_dtype}'. Must be one of {valid_dtypes}.")
5456

superbench/benchmarks/micro_benchmarks/ort_inference_performance.py

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@
1212
import numpy as np
1313

1414
from superbench.common.utils import logger
15-
from superbench.benchmarks import BenchmarkRegistry, Platform, Precision
15+
from superbench.benchmarks import BenchmarkRegistry, Platform, Precision, ReturnCode
1616
from superbench.benchmarks.micro_benchmarks import MicroBenchmark
1717
from superbench.benchmarks.micro_benchmarks.model_source_config import ModelSourceConfig
18-
from superbench.benchmarks.micro_benchmarks.huggingface_model_loader import HuggingFaceModelLoader
18+
from superbench.benchmarks.micro_benchmarks.huggingface_model_loader import (
19+
HuggingFaceModelLoader,
20+
validate_model_identifier,
21+
)
1922

2023

2124
class ORTInferenceBenchmark(MicroBenchmark):
@@ -42,6 +45,9 @@ def __init__(self, name, parameters=''):
4245
]
4346
self.__graph_opt_level = None
4447
self.__model_cache_path = Path(torch.hub.get_dir()) / 'checkpoints'
48+
# Stashed HF config (populated in _preprocess_huggingface_models) so that
49+
# __inference() can derive vocab_size / dynamic input shapes from it.
50+
self._hf_config = None
4551

4652
def add_parser_arguments(self):
4753
"""Add the specified arguments."""
@@ -124,6 +130,24 @@ def add_parser_arguments(self):
124130
help='Sequence length for transformer models.',
125131
)
126132

133+
self._parser.add_argument(
134+
'--require_cuda',
135+
action='store_true',
136+
default=False,
137+
required=False,
138+
help='Fail if CUDAExecutionProvider is not available. '
139+
'Default: warn and fall back to other registered ORT providers (CPU/ROCm/etc.).',
140+
)
141+
142+
self._parser.add_argument(
143+
'--allow_remote_code',
144+
action='store_true',
145+
default=False,
146+
required=False,
147+
help='Allow HuggingFace to execute model-repo Python (trust_remote_code=True). '
148+
'SECURITY: enables RCE from --model_identifier. Pin --revision <sha> when used.',
149+
)
150+
127151
def _preprocess(self):
128152
"""Preprocess/preparation operations before the benchmarking.
129153
@@ -179,8 +203,19 @@ def _preprocess_huggingface_models(self):
179203

180204
if not self._args.model_identifier:
181205
logger.error('--model_identifier is required when using --model_source huggingface')
206+
self._result.set_return_code(ReturnCode.MICROBENCHMARK_EXECUTION_FAILURE)
182207
return False
183208

209+
# Reject malformed / path-like identifiers up front, before any network or disk activity.
210+
try:
211+
validate_model_identifier(self._args.model_identifier)
212+
except ValueError as e:
213+
logger.error(str(e))
214+
self._result.set_return_code(ReturnCode.MICROBENCHMARK_EXECUTION_FAILURE)
215+
return False
216+
217+
allow_remote_code = bool(getattr(self._args, 'allow_remote_code', False))
218+
184219
try:
185220
logger.info(f'Loading HuggingFace model: {self._args.model_identifier}')
186221

@@ -190,13 +225,18 @@ def _preprocess_huggingface_models(self):
190225
load_kwargs = {}
191226
if hf_token:
192227
load_kwargs['token'] = hf_token
193-
hf_config = AutoConfig.from_pretrained(self._args.model_identifier, trust_remote_code=True, **load_kwargs)
228+
hf_config = AutoConfig.from_pretrained(
229+
self._args.model_identifier, trust_remote_code=allow_remote_code, **load_kwargs
230+
)
231+
# Stash for __inference() to read vocab_size / other model metadata later.
232+
self._hf_config = hf_config
194233

195234
precision_str = self._args.precision.value if self._args.precision != Precision.INT8 else 'float32'
196235
fits, param_m, est_gb, avail_gb = HuggingFaceModelLoader.check_memory_fits(
197236
self._args.model_identifier, hf_config, precision_str, mode='inference', token=hf_token
198237
)
199238
if not fits:
239+
self._result.set_return_code(ReturnCode.MICROBENCHMARK_EXECUTION_FAILURE)
200240
return False
201241

202242
# Step 2: Proceed with model download and ONNX export
@@ -217,7 +257,7 @@ def _preprocess_huggingface_models(self):
217257
)
218258

219259
# Load model from HuggingFace on CPU
220-
loader = HuggingFaceModelLoader()
260+
loader = HuggingFaceModelLoader(allow_remote_code=allow_remote_code)
221261
hf_model, _, _ = loader.load_model_from_config(model_config, device='cpu')
222262
from superbench.benchmarks.micro_benchmarks._export_torch_to_onnx import torch2onnxExporter
223263
exporter = torch2onnxExporter()
@@ -237,6 +277,15 @@ def _preprocess_huggingface_models(self):
237277
export_precision = self._args.precision.value
238278
model_name_with_precision = f'{model_name}.{export_precision}'
239279

280+
# Defense-in-depth: confirm the resolved output path stays inside the rank
281+
# directory even though validate_model_identifier already rejected '..' / '\\'.
282+
proc_root = proc_output_path.resolve()
283+
resolved_out = (proc_output_path / f'{model_name_with_precision}.onnx').resolve()
284+
if proc_root not in resolved_out.parents:
285+
logger.error(f'Refusing to write ONNX outside rank dir: {resolved_out} not under {proc_root}')
286+
self._result.set_return_code(ReturnCode.MICROBENCHMARK_EXECUTION_FAILURE)
287+
return False
288+
240289
# Export directly to final destination to avoid path issues with external data
241290
onnx_path = exporter.export_huggingface_model(
242291
model=hf_model,
@@ -248,6 +297,7 @@ def _preprocess_huggingface_models(self):
248297

249298
if not onnx_path:
250299
logger.error(f'Failed to export {self._args.model_identifier} to ONNX')
300+
self._result.set_return_code(ReturnCode.MICROBENCHMARK_EXECUTION_FAILURE)
251301
return False
252302

253303
# Apply INT8 quantization if requested (matching in-house model behavior)
@@ -268,25 +318,32 @@ def _preprocess_huggingface_models(self):
268318
logger.error(f'Failed to prepare HuggingFace model: {str(e)}')
269319
import traceback
270320
logger.error(traceback.format_exc())
321+
self._result.set_return_code(ReturnCode.MICROBENCHMARK_EXECUTION_FAILURE)
271322
return False
272323

273324
def _benchmark(self):
274325
"""Implementation for benchmarking."""
275326
import onnxruntime as ort
276327
precision_metric = {'float16': 'fp16', 'float32': 'fp32', 'int8': 'int8'}
277328

278-
# Require CUDAExecutionProvider — this benchmark targets GPU inference
279329
available = ort.get_available_providers()
280-
if 'CUDAExecutionProvider' not in available:
281-
logger.error(f'CUDAExecutionProvider is not available (available: {available}).')
282-
return False
330+
cuda_available = 'CUDAExecutionProvider' in available
331+
if not cuda_available:
332+
msg = f'CUDAExecutionProvider is not available (available providers: {available}).'
333+
if getattr(self._args, 'require_cuda', False):
334+
logger.error(msg + ' --require_cuda was set, aborting.')
335+
return False
336+
logger.warning(
337+
msg + ' Falling back to registered providers; pass --require_cuda to fail instead.'
338+
)
339+
providers = ['CUDAExecutionProvider'] if cuda_available else available
283340

284341
for model in self._args.pytorch_models:
285342
sess_options = ort.SessionOptions()
286343
sess_options.graph_optimization_level = self.__graph_opt_level[self._args.graph_opt_level]
287344
file_name = '{model}.{precision}.onnx'.format(model=model, precision=self._args.precision)
288345
ort_sess = ort.InferenceSession(
289-
f'{self.__model_cache_path / file_name}', sess_options, providers=['CUDAExecutionProvider']
346+
f'{self.__model_cache_path / file_name}', sess_options, providers=providers
290347
)
291348

292349
elapse_times = self.__inference(ort_sess)
@@ -318,18 +375,30 @@ def __inference(self, ort_sess):
318375
"""
319376
precision = np.float16 if self._args.precision == Precision.FLOAT16 else np.float32
320377

321-
# Get input names from the ONNX session to determine input format
322-
input_names = [input.name for input in ort_sess.get_inputs()]
378+
# Get input metadata from the ONNX session to determine input format and shapes
379+
ort_inputs = ort_sess.get_inputs()
380+
input_names = [inp.name for inp in ort_inputs]
323381

324382
# Determine input format based on what the model expects
325383
if 'pixel_values' in input_names:
326-
# Vision model: use pixel_values (batch_size, 3, 224, 224)
327-
pixel_values = np.random.randn(self._args.batch_size, 3, 224, 224).astype(dtype=precision)
384+
# Vision model: derive (C, H, W) from the exported ONNX graph so that models
385+
# with non-default shapes (e.g. 384x384 ViT, 1-channel medical models) work.
386+
# Fall back to (3, 224, 224) only for dynamic / unknown axes.
387+
meta = next(inp for inp in ort_inputs if inp.name == 'pixel_values')
388+
dims = [d if isinstance(d, int) else None for d in (meta.shape or [])]
389+
# Expected layout is (N, C, H, W); pad to length 4 if shorter.
390+
dims = (dims + [None] * 4)[:4]
391+
_, c, h, w = dims
392+
c, h, w = c or 3, h or 224, w or 224
393+
pixel_values = np.random.randn(self._args.batch_size, c, h, w).astype(dtype=precision)
328394
inputs = {'pixel_values': pixel_values}
329395
elif 'input_ids' in input_names:
330-
# NLP model: use input_ids and attention_mask
396+
# NLP model: use input_ids and attention_mask. Cap token IDs at the model's
397+
# actual vocab_size to avoid out-of-range embedding lookups (undefined behavior
398+
# on CUDA — silent NaNs / device-side asserts).
331399
seq_len = getattr(self._args, 'seq_length', 512)
332-
input_ids = np.random.randint(0, 30000, (self._args.batch_size, seq_len)).astype(np.int64)
400+
vocab_size = getattr(self._hf_config, 'vocab_size', None) or 30000
401+
input_ids = np.random.randint(0, vocab_size, (self._args.batch_size, seq_len)).astype(np.int64)
333402
attention_mask = np.ones((self._args.batch_size, seq_len), dtype=np.int64)
334403
inputs = {'input_ids': input_ids, 'attention_mask': attention_mask}
335404
else:

0 commit comments

Comments
 (0)