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..a55dff7 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,19 +39,21 @@ 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 ['] + # JSON receipts sort object keys; configuration order must not depend on that round trip. out.append(',\n'.join('{ name: "'+name+'" data_type: TYPE_FP32 dims: ['+ - ', '.join(map(str,shape))+'] }' for name,shape in p['outputs'].items())) + ', '.join(map(str,p['outputs'][name]))+'] }' for name in PROFILES[p['key']]['outputs'])) out+= [']',f'instance_group [{{ kind: KIND_GPU count: {count} gpus: [0] }}]', f'dynamic_batching {{ preferred_batch_size: [{p["batch"]}] max_queue_delay_microseconds: 5000 }}'] return '\n'.join(out)+'\n' 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 +69,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 +83,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 +135,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 +311,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/deployment_kit/release.py b/deployment_kit/release.py index 1f20e79..d00d0a9 100644 --- a/deployment_kit/release.py +++ b/deployment_kit/release.py @@ -306,6 +306,8 @@ def deploy(site_path, archive, digest, *, apply=False): from .site import load site=load(site_path); site_hash=sha256(site_path) m=bundle.verify(archive,expected_hash=digest); previous=active(site) + require(not previous or not previous.get('incremental_patch'),'INCREMENTAL_REVIEW_REQUIRED', + '현장 증분 패치가 활성 상태입니다. 해당 패치 원장을 검토한 후 다음 배포를 준비하세요.') pm=None if previous is None else bundle.verify_directory(Path(site['install_root'])/'releases'/previous['release_id']) check_patch(m,previous,pm) require(set(x['service'] for x in m['images'])==set(site['services']),'IMAGE_SPEC','모든 승인 서비스의 image ID를 기록하세요.') @@ -395,6 +397,8 @@ def rollback(site_path, transaction, *, apply=False): from .site import load site=load(site_path); identifier(transaction); current=active(site) require(current is not None,'NO_ACTIVE','활성 release가 없습니다.') + require(not current.get('incremental_patch'),'INCREMENTAL_REVIEW_REQUIRED', + '증분 패치의 image/model/config 원장을 검토한 뒤 명시적 복구를 준비하세요.') tx=state_dir(site)/'transactions'/transaction; target=read_json(tx/'applied.json') path=snapshot_path(site,target); root=Path(site['install_root'])/'releases'/target['release_id'] m=bundle.verify_directory(root) diff --git a/deployment_kit/site.py b/deployment_kit/site.py index dad50fe..130afe6 100644 --- a/deployment_kit/site.py +++ b/deployment_kit/site.py @@ -7,7 +7,7 @@ from . import storage, host from .core import absolute, relative, identifier, require, read_json, write_json, sha256, lock, KitError SERVICES = ('postgresql', 'redis', 'analytics', 'ai-control-sidecar', 'ai-gateway') -MODELS = ('RFDETRMedium', 'RFDETRKeypointPreview', 'RFDETRFireSmoke') +MODELS = ('RFDETRMedium', 'RFDETRKeypointPreview', 'RFDETRFireSmoke', 'Awiros_person_and_head_detection') def validate(value): diff --git a/docs/deployment/MODEL_TOOLING.ko.md b/docs/deployment/MODEL_TOOLING.ko.md index 5748cbb..32d1962 100644 --- a/docs/deployment/MODEL_TOOLING.ko.md +++ b/docs/deployment/MODEL_TOOLING.ko.md @@ -2,6 +2,7 @@ - 모델 변환과 실제 서비스 load는 별도 단계입니다. 변환 성공만으로 운영 모델을 교체하지 않습니다. - Medium/Pose/Fire는 기존 수용 profile에서 설정을 생성합니다. 오래된 repository pbtxt는 복사하지 않습니다. +- CROWD는 승인된 Awiros ONNX의 별도512 profile을 사용합니다. `model all`은 기존3종 범위를 유지합니다. - A6000용 plan을 Blackwell에서 무검증 재사용하지 않습니다. 대상 GPU와 같은 TensorRT에서 검증합니다. - 실제 image·ONNX·reviewed exporter는 담당자가 제공합니다. 자동 인터넷 다운로드는 없습니다. @@ -17,6 +18,7 @@ | 대기 | 5000us | dynamic batch 대기 설정 | 기본 이름은 RFDETRMedium, RFDETRKeypointPreview, RFDETRFireSmoke입니다. +추가 CROWD 이름은 Awiros_person_and_head_detection이며 dets[300,4]/labels[300,3], 입력512입니다. 출력은 Medium dets[300,4]/labels[300,91], Pose dets[100,4]/labels[100,2]/keypoints[100,34,8], Fire dets[300,4]/labels[300,3]. 이 profile은 3모델을 24GB GPU에 동시에 instance8로 load해도 된다는 보장이 아닙니다. diff --git a/docs/profiles/ltb1021/topics/runtime-lifecycle/README.ko.md b/docs/profiles/ltb1021/topics/runtime-lifecycle/README.ko.md index 3b55c76..d8932e7 100644 --- a/docs/profiles/ltb1021/topics/runtime-lifecycle/README.ko.md +++ b/docs/profiles/ltb1021/topics/runtime-lifecycle/README.ko.md @@ -1,10 +1,23 @@ # Runtime Lifecycle -Status: `FIGHT/CROWD WEB READY · 22 CAMERAS STOPPED · HUMAN START WAIT` +Status: `ICERTI INCREMENTAL READY · 4 MODELS READY · 3 CAMERAS STOPPED · HUMAN START WAIT` Owner: `(test, ltb1021, work-session)` on `task/runtime-lifecycle-refactor` -Last reviewed: `2026-09-20 KST` +Last reviewed: `2026-09-21 KST` + +## 최신 현장 증분 반영 + +[현장 기록](runs/20260921T104408+0900-run-icerti-incremental.md), +[증분 계약](decision/20260921T104408+0900-decision-icerti-incremental.md), +[현장 가이드](../../../../deployment/ICERTI_INCREMENTAL.ko.md). +침입/배회/FIRE/FALL/FIGHT/CROWD source/native를 기존 현장 base image 위에 반영했다. +Medium 재사용, Pose/Fire/CROWD Blackwell 변환·batch1~4 shape/dtype/finite PASS. +모델4종 instance8/batch4/5ms READY, idle VRAM4778/24467MiB이며 부하 시험은 아니다. +WSS bootstrap, Redis reader/관리명령 차단, Artifact On/HTTP On, 3채널 stopped 확인. +FALL 현장 기본은DOWN 대기 생략On, 공용 기본Off와 명시적 채널Off를 보존한다. +인증/원장/비대상 서비스 보존. 실제 카메라Start/Event/브라우저 수용은 사용자 시험 대기다. +기존 FULL과 실패 증거는 보존하며 아래 capsule은 각 시점의 기록이다. ## 최신 FIGHT/CROWD Web 준비 diff --git a/docs/scenarios/SCENARIO_WEB_READY.ko.md b/docs/scenarios/SCENARIO_WEB_READY.ko.md index e29badf..91986e2 100644 --- a/docs/scenarios/SCENARIO_WEB_READY.ko.md +++ b/docs/scenarios/SCENARIO_WEB_READY.ko.md @@ -79,6 +79,10 @@ person ROI점은 bbox 하단중앙, head는 중심이다. 현재 각 클래스 ROI/score/관절 geometry/전이 만료/episode 1회/recovery/run 분리와 Event 인계는 그대로다. 처음부터 누운 사람이나 단순 DOWN 자세만으로 Event를 생성하는 옵션이 아니다. +현장별 승인 기본값은 `CONTEXTRA_FALL_SKIP_DOWN_HOLD_DEFAULT=true|false`로 지정할 수 있다. +미설정 기본은false이며 명시적 채널 설정이 우선한다. 아이서티는 승인에 따라 현장 기본true를 +적용했다. test 기본값/기존 시험채널은 변경하지 않았다. [현장 적용/복귀](../deployment/ICERTI_INCREMENTAL.ko.md)를 따른다. + Web에는 새 필드를 추가하지 않는다. 기존 Start projection은 이 내부 옵션을 보존하지만, FALL confidence는 Web 입력이 우선하므로 시험에서는 `0.30`을 지정한다. 기본 Off와 특정 채널 On을 혼동하지 않는다. bbox 검출 개선이나 실제 낙상 정확도를 보장하지 않으며, 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)