diff --git a/app/fall_runtime.py b/app/fall_runtime.py index c787834..5c2da84 100644 --- a/app/fall_runtime.py +++ b/app/fall_runtime.py @@ -1,5 +1,6 @@ """FALL-WIRE-01: local ROI/decision adapter; one owner, no tracker or delivery I/O.""" from dataclasses import dataclass, fields +import os import time import uuid @@ -22,7 +23,13 @@ class FallSettings: roi_names = {'roi', 'roi_id', 'roi_name', 'debug_roi'} if set(value) - names - roi_names: raise ValueError('Invalid FALL settings fields.') - policy = FallConfig.parse({k: v for k, v in value.items() if k in names}) + values = {k: v for k, v in value.items() if k in names} + # Site default applies only to absent values; an explicit per-camera Off wins. + default = os.environ.get('CONTEXTRA_FALL_SKIP_DOWN_HOLD_DEFAULT', 'false') + if default not in ('true', 'false'): + raise ValueError('Invalid FALL site default.') + values.setdefault('test_skip_down_hold', default == 'true') + policy = FallConfig.parse(values) roi = value.get('roi') geometry = IntrusionConfig(enabled=policy.enabled, confidence=policy.person_score, roi=((0., 0.), (1., 0.), (1., 1.), (0., 1.)) if roi is None or roi == [] else roi, diff --git a/deployment_kit/engine_contract.cpp b/deployment_kit/engine_contract.cpp index 4b1fdd7..6f1828d 100644 --- a/deployment_kit/engine_contract.cpp +++ b/deployment_kit/engine_contract.cpp @@ -26,11 +26,12 @@ int main(int argc, char** argv) { try { require(argc==4 && NV_TENSORRT_MAJOR==10 && NV_TENSORRT_MINOR==16); const std::string key=argv[2]; const int batch=std::stoi(argv[3]); - require((key=="medium" || key=="pose" || key=="fire") && batch>=1 && batch<=8); + require((key=="medium" || key=="pose" || key=="fire" || key=="crowd") && batch>=1 && batch<=8); const bool pose=key=="pose"; + const int r=key=="crowd" ? 512 : 576; std::map> expected={ - {"input",{-1,3,576,576}}, {"dets",{-1,pose ? 100 : 300,4}}, - {"labels",{-1,pose ? 100 : 300,pose ? 2 : key=="fire" ? 3 : 91}}}; + {"input",{-1,3,r,r}}, {"dets",{-1,pose ? 100 : 300,4}}, + {"labels",{-1,pose ? 100 : 300,pose ? 2 : key=="medium" ? 91 : 3}}}; if (pose) expected["keypoints"]={-1,100,34,8}; std::ifstream file(argv[1],std::ios::binary); require(file.good()); std::vector bytes((std::istreambuf_iterator(file)),{}); @@ -51,11 +52,11 @@ int main(int argc, char** argv) { const nvinfer1::OptProfileSelector selectors[]={nvinfer1::OptProfileSelector::kMIN, nvinfer1::OptProfileSelector::kOPT,nvinfer1::OptProfileSelector::kMAX}; for (int i=0; i<3; ++i) - require(shape(engine->getProfileShape("input",0,selectors[i]),{i==0 ? 1 : batch,3,576,576})); + require(shape(engine->getProfileShape("input",0,selectors[i]),{i==0 ? 1 : batch,3,r,r})); std::cout << "{\"contract\":\"PASS\",\"backend\":\"TensorRT_CXX\",\"tensorrt_version\":\"" << NV_TENSORRT_MAJOR << '.' << NV_TENSORRT_MINOR << '.' << NV_TENSORRT_PATCH - << "\",\"batch_bounds\":[[1,3,576,576],[" << batch << ",3,576,576],[" - << batch << ",3,576,576]]}" << std::endl; + << "\",\"batch_bounds\":[[1,3," << r << ',' << r << "],[" << batch << ",3," << r << ',' << r << "],[" + << batch << ",3," << r << ',' << r << "]]}" << std::endl; return 0; } catch (const std::exception&) { std::cerr << "ENGINE_CONTRACT_FAILED\n"; diff --git a/deployment_kit/model_probe.py b/deployment_kit/model_probe.py index 5841062..36c46a5 100644 --- a/deployment_kit/model_probe.py +++ b/deployment_kit/model_probe.py @@ -20,7 +20,8 @@ def onnx_contract(path, profile): dims=[d.dim_value if d.HasField('dim_value') else -1 for d in t.shape.dim] return {'dtype':t.elem_type,'shape':dims} tensors={x.name:desc(x) for x in (*model.graph.input,*model.graph.output)} - expected={'input':[-1,3,576,576], **{k:[-1,*v] for k,v in profile['outputs'].items()}} + r=profile['resolution'] + expected={'input':[-1,3,r,r], **{k:[-1,*v] for k,v in profile['outputs'].items()}} if set(tensors)!=set(expected): raise ValueError('ONNX_NAMES') for name,shape in expected.items(): actual=tensors[name]['shape'] @@ -47,7 +48,8 @@ def engine_contract(path, profile): with trt.Runtime(logger) as runtime: engine=runtime.deserialize_cuda_engine(Path(path).read_bytes()) if engine is None: raise ValueError('ENGINE_DESERIALIZE') - expected={'input':[-1,3,576,576], **{k:[-1,*v] for k,v in profile['outputs'].items()}} + r=profile['resolution'] + expected={'input':[-1,3,r,r], **{k:[-1,*v] for k,v in profile['outputs'].items()}} actual={} for i in range(engine.num_io_tensors): name=engine.get_tensor_name(i) @@ -60,7 +62,7 @@ def engine_contract(path, profile): if list(engine.get_tensor_shape(name))!=shape or engine.get_tensor_dtype(name)!=dtype or engine.get_tensor_mode(name)!=mode: raise ValueError('ENGINE_CONTRACT') bounds=[list(s) for s in engine.get_tensor_profile_shape('input',0)] - if bounds!=[[b,3,576,576] for b in (1,profile['batch'],profile['batch'])]: + if bounds!=[[b,3,r,r] for b in (1,profile['batch'],profile['batch'])]: raise ValueError('ENGINE_BATCH_PROFILE') return {'tensorrt_version':trt.__version__,'tensors':actual,'batch_bounds':bounds} @@ -74,7 +76,7 @@ def smoke(url,profile): results=[] try: for batch in range(1,profile['batch']+1): - data=np.zeros((batch,3,576,576),dtype=np.float16) + data=np.zeros((batch,3,profile['resolution'],profile['resolution']),dtype=np.float16) inp=client.InferInput('input',data.shape,'FP16'); inp.set_data_from_numpy(data) rid='deployment-contract-'+str(batch) result=c.infer(profile['name'],[inp],request_id=rid,client_timeout=5) diff --git a/deployment_kit/models.py b/deployment_kit/models.py index c9ddf00..8d24ab2 100644 --- a/deployment_kit/models.py +++ b/deployment_kit/models.py @@ -18,15 +18,17 @@ PROFILES = { 'source_onnx':'66ebb0386fefc400d4bd89fe74cd840d526f179b13ca73cd73850993ceb4aba5'}, 'fire': {'name':'RFDETRFireSmoke','outputs':{'dets':[300,4],'labels':[300,3]}, 'source_onnx':'56b509d8d7163c8e8a3d41eab76d81fb62bb8a16ac204be2db5619ee7c362482'}, + 'crowd': {'name':'Awiros_person_and_head_detection','outputs':{'dets':[300,4],'labels':[300,3]}, + 'source_onnx':'98f5c29c60c4bb60120e26e0287bea456c1bb503b4c2d21f4b0d20d8e47868dc'}, } def profile(name, *, instances=8, batch=4, workspace=1024): - require(name in PROFILES,'MODEL_UNKNOWN','medium/pose/fire 중 하나를 선택하세요.') + require(name in PROFILES,'MODEL_UNKNOWN','medium/pose/fire/crowd 중 하나를 선택하세요.') require(type(instances) is int and 1<=instances<=16 and type(batch) is int and 1<=batch<=8 and type(workspace) is int and 256<=workspace<=8192,'MODEL_OPTIONS','instance1~16 / batch1~8 / workspace256~8192 MiB 범위를 사용하세요.') return dict(copy.deepcopy(PROFILES[name]),key=name,instances=instances,batch=batch,workspace_mib=workspace, - input_dtype='FP16',output_dtype='FP32',engine='FP16_OPTIMIZED',resolution=576, + input_dtype='FP16',output_dtype='FP32',engine='FP16_OPTIMIZED',resolution=512 if name=='crowd' else 576, queue_delay_us=5000,container_gpu=0,source_input_dtype='FP32', deployment_note='instance8은 기존 단독모델 시험 기준. Blackwell24GB 세모델 동시 수용을 보장하지 않음.') @@ -37,7 +39,7 @@ def pbtxt(p, *, model_version=1, instances=None): require(type(count) is int and 1<=count<=16,'MODEL_OPTIONS','instance1~16을 사용하세요.') out=[f'name: "{p["name"]}"','platform: "tensorrt_plan"',f'max_batch_size: {p["batch"]}', f'version_policy {{ specific {{ versions: [{model_version}] }} }}', - 'input [{ name: "input" data_type: TYPE_FP16 dims: [3, 576, 576] }]', 'output ['] + 'input [{ name: "input" data_type: TYPE_FP16 dims: [3, '+str(p['resolution'])+', '+str(p['resolution'])+'] }]', 'output ['] out.append(',\n'.join('{ name: "'+name+'" data_type: TYPE_FP32 dims: ['+ ', '.join(map(str,shape))+'] }' for name,shape in p['outputs'].items())) out+= [']',f'instance_group [{{ kind: KIND_GPU count: {count} gpus: [0] }}]', @@ -46,10 +48,11 @@ def pbtxt(p, *, model_version=1, instances=None): def trtexec_args(p): + r=p['resolution'] return ['/usr/bin/trtexec','--onnx=/input/model.onnx','--saveEngine=/output/model.plan','--fp16', '--inputIOFormats=fp16:chw','--outputIOFormats='+','.join('fp32:chw' for _ in p['outputs']), - '--minShapes=input:1x3x576x576',f'--optShapes=input:{p["batch"]}x3x576x576', - f'--maxShapes=input:{p["batch"]}x3x576x576',f'--memPoolSize=workspace:{p["workspace_mib"]}', + f'--minShapes=input:1x3x{r}x{r}',f'--optShapes=input:{p["batch"]}x3x{r}x{r}', + f'--maxShapes=input:{p["batch"]}x3x{r}x{r}',f'--memPoolSize=workspace:{p["workspace_mib"]}', '--skipInference','--profilingVerbosity=detailed'] @@ -65,7 +68,7 @@ def _helper_args(image, name, mounts, gpu=None): return args -def _once(image, mounts, command, *, gpu=None, timeout=900): +def _once(image, mounts, command, *, gpu=None, timeout=900, evidence=None): name='contextra-model-'+uuid.uuid4().hex[:12] args=_helper_args(image,name,mounts,gpu) args[0]='create'; args[-2]=command[0] @@ -79,7 +82,13 @@ def _once(image, mounts, command, *, gpu=None, timeout=900): require(json.loads(state).get('ExitCode')==0,'MODEL_HELPER_FAILED','모델 helper가 실패했습니다. 임의 재시도하지 않습니다.') return raw finally: - cleanup_helper(name,cid) + try: + if cid and evidence is not None: + _,logs=docker('logs','--tail','2000',cid,check=False,timeout=10, + limit=4*1024**2,merge_output=True) + atomic_write(evidence,logs,mode=0o600) + finally: + cleanup_helper(name,cid) def serialized_gpu(function): @@ -125,7 +134,7 @@ def build(name, onnx_path, output, image, *, gpu=0, apply=False, instances=8, ba raw=_once(iid,mounts,[*base,'onnx','--path','/input/model.onnx','--profile','/output/profile.json'],timeout=60) onnx_check=json.loads(raw.strip().splitlines()[-1]) _,gpu_raw=run(['nvidia-smi',f'--id={gpu}','--query-gpu=name,uuid,driver_version','--format=csv,noheader']) - build_log=_once(iid,mounts,trtexec_args(p),gpu=gpu) + build_log=_once(iid,mounts,trtexec_args(p),gpu=gpu,evidence=out/'build-container.log') atomic_write(out/'build.log',build_log,mode=0o600) raw=_once(iid,mounts,[*base,'engine','--path','/output/model.plan','--profile','/output/profile.json'],gpu=gpu,timeout=60) engine_check=json.loads(raw.strip().splitlines()[-1]) @@ -301,14 +310,15 @@ def export_weight(name,weight,output,image,adapter,adapter_hash,*,apply=False): def prepare_all(mapping,output,image,*,gpu=0,apply=False): inputs=read_json(absolute(mapping,no_symlinks=True)) - require(set(inputs)==set(PROFILES),'MODEL_INPUT_MAP','medium/pose/fire 각각의 ONNX 경로를 지정하세요.') + names=('medium','pose','fire') + require(set(inputs)==set(names),'MODEL_INPUT_MAP','medium/pose/fire 각각의 ONNX 경로를 지정하세요.') root=absolute(output,no_symlinks=True) - plans={n:build(n,inputs[n],root/n,image,gpu=gpu) for n in PROFILES} - if not apply:return {'sequence':list(PROFILES),'plans':plans,'parallel_build':False,'production_load':False} + plans={n:build(n,inputs[n],root/n,image,gpu=gpu) for n in names} + if not apply:return {'sequence':list(names),'plans':plans,'parallel_build':False,'production_load':False} require(not root.exists(),'FILE_EXISTS','전체 변환 작업에는 새 폴더를 사용하세요.') root.mkdir(parents=True,mode=0o700) results={} - for n in PROFILES: + for n in names: build(n,inputs[n],root/n,image,gpu=gpu,apply=True) results[n]=verify_candidate(root/n,image,gpu=gpu,apply=True) return {'results':results,'production_load':False} diff --git a/tests/deployment/test_models.py b/tests/deployment/test_models.py index cdc27d6..58ae850 100644 --- a/tests/deployment/test_models.py +++ b/tests/deployment/test_models.py @@ -25,7 +25,7 @@ class ModelTests(unittest.TestCase): def test_all_profiles(self): for name in models.PROFILES: with self.subTest(name=name): - p=models.profile(name); self.assertEqual((p['instances'],p['batch'],p['resolution']),(8,4,576)) + p=models.profile(name); self.assertEqual((p['instances'],p['batch'],p['resolution']),(8,4,512 if name=='crowd' else 576)) c=models.pbtxt(p) self.assertIn('TYPE_FP16',c);self.assertIn('TYPE_FP32',c);self.assertIn('max_batch_size: 4',c) self.assertIn('count: 8',c);self.assertIn('5000',c)