# P&ID 도면 파싱 병렬 LLM 아키텍처 개선안 (수정본 v3) ## 1. 기존 문제점 분석 ### 1.1 DXF 파일 엔티티 분포 | 엔티티 타입 | 수량 | 비율 | 처리 방식 | |------------|------|------|-----------| | LINE | 20,868 | 72.4% | 기하학적 추출 (GPU 불필요) | | TEXT | 3,562 | 12.4% | LLM 매핑 필요 | | ARC | 1,324 | 4.6% | 기하학적 추출 (GPU 불필요) | | CIRCLE | 1,275 | 4.4% | 기하학적 추출 (GPU 불필요) | | LWPOLYLINE | 865 | 3.0% | 기하학적 추출 (GPU 불필요) | | MTEXT | 363 | 1.3% | LLM 매핑 필요 | | ELLIPSE | 190 | 0.7% | 기하학적 추출 (GPU 불필요) | | HATCH | 103 | 0.4% | 기하학적 추출 (GPU 불필요) | | INSERT | 103 | 0.4% | 기하학적 추출 (GPU 불필요) | | SOLID | 77 | 0.3% | 기하학적 추출 (GPU 불필요) | | SPLINE | 65 | 0.2% | 기하학적 추출 (GPU 불필요) | | POINT | 17 | 0.1% | 기하학적 추출 (GPU 불필요) | | POLYLINE | 4 | 0.0% | 기하학적 추출 (GPU 불필요) | | LEADER | 2 | 0.0% | 기하학적 추출 (GPU 불필요) | | OLE2FRAME | 1 | 0.0% | 기하학적 추출 (GPU 불필요) | **총 엔티티 수**: 28,819개 **핵심 발견**: - **LINE이 72.4%**를 차지 → 배관 처리가 핵심 병목 - TEXT/MTEXT만 13.7% → LLM 매핑은 TEXT 중심 - LINE은 기하학적 추출만으로 충분 (LLM 불필요) ### 1.2 현재 구조의 병목 | 단계 | 문제점 | 심각도 | |------|--------|--------| | Phase 1 | ezdxf로 28,000개 엔티티 처리 | 0.58초 (양호) | | Phase 2 | O(n²) 노드 병합 | timeout (심각) | | Phase 3 | 순차적 LLM API 호출 | 예측 불가능한 지연 | ### 1.2 test_dxf_extract_pid*.py의 성공적인 병렬 처리 구조 ```python # test_dxf_extract_pid1.py, pid2.py, pid3.py의 공통 구조 chunks = [ { 'name': 'Field Instruments - Sensors', 'system': 'Extract sensor tags only...', 'user': 'Extract ALL tags of FT, FIT, LT, PT...' }, { 'name': 'Field Instruments - Valves', 'system': 'Extract valve tags only...', 'user': 'Extract ALL tags of FCV, TCV, LCV...' }, { 'name': 'System Tags', 'system': 'Extract system tags only...', 'user': 'Extract ALL tags of LI, PI, TI...' } ] ``` **핵심 발견**: - **청크 단위 분할**: 태그 유형별로 프롬프트를 분리 - **독립된 프로세스 실행**: 각 청크를 별도의 Python 프로그램으로 실행 - **vLLM GPU 자원 최대화**: 각 프로세스가 별도의 GPU에 할당됨 --- ## 2. 정확한 병렬 처리 전략 ### 2.1 vLLM의 tensor parallelism 이해 **vLLM의 병렬 처리 방식**: - **tensor parallelism**: 단일 프로세스 내에서 여러 GPU 카드를 사용 - **multi-process**: 여러 프로세스가 각각 별도의 GPU 카드를 사용 **문제점**: - 하나의 프로세스에서 여러 요청을 보낼 경우 → **단일 GPU에만 할당** - 여러 프로세스에서 각각 요청을 보낼 경우 → **각 GPU에 별도로 할당** **해결책**: - test_dxf_extract_pid*.py처럼 **각 청크를 독립된 프로그램으로 실행** - `python pid_extractor_sensor.py & python pid_extractor_valve.py & python pid_extractor_system.py &` ### 2.2 병렬 실행 구조 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ P&ID 도면 파싱 파이프라인 (병렬 LLM + LINE 처리) │ └─────────────────────────────────────────────────────────────────────┘ Phase 1: 기하학적 추출 (ezdxf) ├─ DXF 파일 로드 (0.84초) ├─ 엔티티별 BBox 계산 (0.58초) │ ├─ LINE (20,868개) → 배관 선 추출 │ ├─ TEXT (3,562개) → 태그명 추출 │ ├─ ARC/CIRCLE (2,599개) → 기하학적 도형 │ └─ 기타 (1,790개) → 기하학적 도형 └─ 결과: 28,257개 GeometricEntity Phase 2: 위상 빌더 (공간 인덱스) ├─ 공간 인덱스 생성 (R-tree) ├─ 노드 병합 (O(n log n)) │ ├─ LINE 병합 → 배관 연결 │ └─ TEXT 병합 → 태그명 정제 └─ 결과: NetworkX 그래프 Phase 3: 병렬 LLM 매핑 (독립 프로그램 실행) ├─ 프로그램 1: pid_extractor_text.py (GPU 0) │ ├─ 프롬프트: "Extract all tag names from TEXT entities" │ └─ 결과: 3,562개 태그명 │ ├─ 프로그램 2: pid_extractor_valve.py (GPU 1) │ ├─ 프롬프트: "Extract valve tags (FCV, TCV, LCV, PCV, XV, ...)" │ └─ 결과: 80개 태그 │ ├─ 프로그램 3: pid_extractor_equipment.py (GPU 2) │ ├─ 프롬프트: "Extract equipment tags (Pump, Tank, Heat Exchanger)" │ └─ 결과: 50개 태그 │ ├─ 프로그램 4: pid_extractor_system.py (GPU 3) │ ├─ 프롬프트: "Extract system tags (FICQ, TICA, PICA, ...)" │ └─ 결과: 120개 태그 │ └─ 결과 병합: 3,812개 매핑된 태그 ``` ### 2.3 LINE (배관) 처리 전략 **문제점**: - LINE이 20,868개 (72.4%)로 압도적으로 많음 - LINE은 태그명이 없으므로 LLM 매핑 불필요 - LINE은 기하학적 추출 + 공간 인덱스로 처리 **해결책**: 1. **Phase 1**: ezdxf로 LINE 추출 → 좌표 저장 2. **Phase 2**: R-tree로 LINE 병합 → 배관 연결 3. **Phase 3**: LLM 매핑 불필요 (기하학적 연결만 사용) **구현 예시**: ```python # LINE 추출 (ezdxf) for entity in msp: if entity.dxftype() == 'LINE': start = entity.dxf.start end = entity.dxf.end line_data = { 'entity_id': entity.dxf.handle, 'entity_type': 'LINE', 'start': (start.x, start.y), 'end': (end.x, end.y), 'length': ((end.x - start.x)**2 + (end.y - start.y)**2)**0.5 } lines.append(line_data) # LINE 병합 (R-tree) # 인접 LINE을 연결하여 배관 경로 생성 ``` ### 2.4 병렬 실행 스크립트 (run_parallel_extract.sh) ```bash #!/bin/bash # P&ID 태그 추출 병렬 실행 스크립트 DXF_FILE="/path/to/pid.dxf" OUTPUT_DIR="/path/to/output" # Phase 1: 기하학적 추출 (ezdxf) echo "Phase 1: 기하학적 추출 시작..." python pid_geometric_extractor.py "$DXF_FILE" "$OUTPUT_DIR/geometric_data.json" echo "Phase 1 완료: geometric_data.json 저장" # Phase 2: 위상 빌더 (공간 인덱스) echo "Phase 2: 위상 빌더 시작..." python pid_topology_builder.py "$OUTPUT_DIR/geometric_data.json" "$OUTPUT_DIR/topology.json" echo "Phase 2 완료: topology.json 저장" # Phase 3: 병렬 LLM 매핑 (4개 프로그램 동시에 실행) echo "Phase 3: 병렬 LLM 매핑 시작..." # GPU 0: TEXT 태그 추출 python pid_extractor_text.py "$DXF_FILE" "$OUTPUT_DIR" & # GPU 1: VALVE 태그 추출 python pid_extractor_valve.py "$DXF_FILE" "$OUTPUT_DIR" & # GPU 2: EQUIPMENT 태그 추출 python pid_extractor_equipment.py "$DXF_FILE" "$OUTPUT_DIR" & # GPU 3: SYSTEM 태그 추출 python pid_extractor_system.py "$DXF_FILE" "$OUTPUT_DIR" & # 모든 프로세스가 완료될 때까지 대기 wait # 결과 병합 echo "결과 병합 시작..." python merge_results.py "$OUTPUT_DIR" "$OUTPUT_DIR/merged_tags.json" echo "Phase 3 완료: merged_tags.json 저장" echo "전체 파이프라인 완료!" ``` --- ## 3. 상세 구현 계획 ### 3.1 Phase 1: 기하학적 추출 (변경 없음) ```python # pid_geometric_extractor.py (현재 그대로 사용) class PidGeometricExtractor: def __init__(self, file_path: str): self.doc = ezdxf.readfile(file_path) self.msp = self.doc.modelspace() def extract_and_save(self, output_path: str): results = [] for entity in self.msp: bbox_obj = self.get_bbox(entity) # ... 추출 로직 return results ``` ### 3.2 Phase 2: 위상 빌더 (공간 인덱스 도입) ```python # pid_topology_builder.py (개선안) from rtree import index class PidTopologyBuilder: def __init__(self, geometric_data: List[Dict[str, Any]]): self.data = geometric_data self.G = nx.DiGraph() def build_graph(self): # 1. 공간 인덱스 생성 self._build_spatial_index() # 2. 노드 병합 (R-tree 사용) self._merge_nodes_spatial() # 3. 태그-설비 연결 self._link_tags_to_equipment() # 4. 배관 연결 self._link_pipes() def _build_spatial_index(self): """R-tree 공간 인덱스 생성""" p = index.Property() self.idx = index.Index(properties=p) for i, item in enumerate(self.data): bbox = item['bbox'] self.idx.insert(i, ( bbox['min_x'], bbox['min_y'], bbox['max_x'], bbox['max_y'] )) def _merge_nodes_spatial(self): """공간 인덱스를 사용한 병합 (O(n log n))""" merge_threshold = 2.0 merged = [] visited = set() for i, item in enumerate(self.data): if i in visited: continue bbox = item['bbox'] # 인접 노드만 검색 neighbors = list(self.idx.intersection(( bbox['min_x'] - merge_threshold, bbox['min_y'] - merge_threshold, bbox['max_x'] + merge_threshold, bbox['max_y'] + merge_threshold ))) # ... 병합 로직 ``` ### 3.3 Phase 3: 병렬 LLM 매핑 (신규 구현) #### 3.3.0 pid_extractor_line.py (LINE 배관 추출 - GPU 불필요) ```python #!/usr/bin/env python3 """ P&ID 도면에서 LINE (배관) 추출 (CPU 전용, GPU 불필요) """ import ezdxf import json import sys def main(): if len(sys.argv) != 3: print("Usage: python pid_extractor_line.py ") sys.exit(1) dxf_file = sys.argv[1] output_dir = sys.argv[2] # DXF 파일 읽기 doc = ezdxf.readfile(dxf_file) msp = doc.modelspace() # LINE 추출 lines = [] for entity in msp: if entity.dxftype() == 'LINE': start = entity.dxf.start end = entity.dxf.end line_data = { 'entity_id': entity.dxf.handle, 'entity_type': 'LINE', 'start': (start.x, start.y), 'end': (end.x, end.y), 'length': ((end.x - start.x)**2 + (end.y - start.y)**2)**0.5 } lines.append(line_data) # 결과 저장 output_file = f'{output_dir}/line_data.json' with open(output_file, 'w', encoding='utf-8') as f: json.dump(lines, f, indent=2, ensure_ascii=False) print(f'LINE 추출 완료: {len(lines)}개') if __name__ == '__main__': main() ``` #### 3.3.1 pid_extractor_text.py (TEXT 태그 추출 - GPU 0) ```python #!/usr/bin/env python3 """ P&ID 도면에서 TEXT 태그 추출 (GPU 0 전용) """ import ezdxf import json import sys from ezdxf.tools.text import plain_mtext from openai import OpenAI def main(): if len(sys.argv) != 3: print("Usage: python pid_extractor_text.py ") sys.exit(1) dxf_file = sys.argv[1] output_dir = sys.argv[2] # DXF 파일 읽기 doc = ezdxf.readfile(dxf_file) msp = doc.modelspace() # 텍스트 추출 texts = [] for entity in msp: if entity.dxftype() == 'TEXT': texts.append(entity.dxf.text) elif entity.dxftype() == 'MTEXT': try: plain = plain_mtext(entity.dxf.text) if plain.strip(): texts.append(plain) except Exception: pass text = '\n'.join(texts) # OpenAI 클라이언트 생성 llm = OpenAI( base_url='http://localhost:8000/v1', api_key='dummy', timeout=1800 ) # 프롬프트 system = ( 'You are a P&ID expert. Extract all tag names from TEXT entities.\n' 'Return ONLY a JSON array.\n' '\n' 'Format: [{"tagNo":"FICQ-10101","confidence":0.95},...]\n' ) user = f'Extract ALL tag names from the text below:\n\n{text[:100000]}' # LLM 호출 resp = llm.chat.completions.create( model='Qwen/Qwen3-Coder-Next-FP8', messages=[ {'role': 'system', 'content': system}, {'role': 'user', 'content': user}, ], max_tokens=65536, temperature=0.1, extra_body={'chat_template_kwargs': {'enable_thinking': False}}, ) # 결과 저장 raw = (resp.choices[0].message.content or '').strip() # ... JSON 파싱 및 저장 로직 output_file = f'{output_dir}/text_tags.json' with open(output_file, 'w', encoding='utf-8') as f: json.dump(all_tags, f, indent=2, ensure_ascii=False) if __name__ == '__main__': main() ``` #### 3.3.2 pid_extractor_valve.py (Valve 태그 추출 - GPU 1) ```python #!/usr/bin/env python3 """ P&ID 도면에서 Sensor 태그 추출 (GPU 0 전용) """ import ezdxf import json import sys from ezdxf.tools.text import plain_mtext from openai import OpenAI def main(): if len(sys.argv) != 3: print("Usage: python pid_extractor_sensor.py ") sys.exit(1) dxf_file = sys.argv[1] output_dir = sys.argv[2] # DXF 파일 읽기 doc = ezdxf.readfile(dxf_file) msp = doc.modelspace() # 텍스트 추출 texts = [] for entity in msp: if entity.dxftype() == 'TEXT': texts.append(entity.dxf.text) elif entity.dxftype() == 'MTEXT': try: plain = plain_mtext(entity.dxf.text) if plain.strip(): texts.append(plain) except Exception: pass text = '\n'.join(texts) # OpenAI 클라이언트 생성 llm = OpenAI( base_url='http://localhost:8000/v1', api_key='dummy', timeout=1800 ) # 프롬프트 system = ( 'You are a P&ID expert. Extract sensor tags only.\n' 'Return ONLY a JSON array.\n' '\n' 'Instrument types to extract: FT, FIT, LT, PT, TE, PG, LG, TG\n' 'Format: [{"tagNo":"FT-10101","confidence":0.95},...]\n' ) user = f'Extract ALL tags of FT, FIT, LT, PT, TE, PG, LG, TG from the text below:\n\n{text[:100000]}' # LLM 호출 resp = llm.chat.completions.create( model='Qwen/Qwen3-Coder-Next-FP8', messages=[ {'role': 'system', 'content': system}, {'role': 'user', 'content': user}, ], max_tokens=65536, temperature=0.1, extra_body={'chat_template_kwargs': {'enable_thinking': False}}, ) # 결과 저장 raw = (resp.choices[0].message.content or '').strip() # ... JSON 파싱 및 저장 로직 output_file = f'{output_dir}/sensor_tags.json' with open(output_file, 'w', encoding='utf-8') as f: json.dump(all_tags, f, indent=2, ensure_ascii=False) if __name__ == '__main__': main() ``` #### 3.3.2 pid_extractor_valve.py (Valve 태그 추출) ```python #!/usr/bin/env python3 """ P&ID 도면에서 Valve 태그 추출 (GPU 1 전용) """ import ezdxf import json import sys from ezdxf.tools.text import plain_mtext from openai import OpenAI def main(): if len(sys.argv) != 3: print("Usage: python pid_extractor_valve.py ") sys.exit(1) dxf_file = sys.argv[1] output_dir = sys.argv[2] # ... 동일한 로직 (프롬프트만 다름) system = ( 'You are a P&ID expert. Extract valve tags only.\n' 'Return ONLY a JSON array.\n' '\n' 'Instrument types to extract: FCV, TCV, LCV, PCV, XV, FV, LV, PV, TV\n' 'Format: [{"tagNo":"FCV-10101","confidence":0.95},...]\n' ) # ... 나머지 로직 ``` #### 3.3.3 pid_extractor_equipment.py (Equipment 태그 추출) ```python #!/usr/bin/env python3 """ P&ID 도면에서 Equipment 태그 추출 (GPU 2 전용) """ import ezdxf import json import sys from ezdxf.tools.text import plain_mtext from openai import OpenAI def main(): if len(sys.argv) != 3: print("Usage: python pid_extractor_equipment.py ") sys.exit(1) dxf_file = sys.argv[1] output_dir = sys.argv[2] # ... 동일한 로직 (프롬프트만 다름) system = ( 'You are a P&ID expert. Extract equipment tags only.\n' 'Return ONLY a JSON array.\n' '\n' 'Equipment types to extract: Pump, Tank, Heat Exchanger, Vessel, Column\n' 'Format: [{"tagNo":"P-101","confidence":0.95},...]\n' ) # ... 나머지 로직 ``` #### 3.3.4 pid_extractor_system.py (System 태그 추출) ```python #!/usr/bin/env python3 """ P&ID 도면에서 System 태그 추출 (GPU 3 전용) """ import ezdxf import json import sys from ezdxf.tools.text import plain_mtext from openai import OpenAI def main(): if len(sys.argv) != 3: print("Usage: python pid_extractor_system.py ") sys.exit(1) dxf_file = sys.argv[1] output_dir = sys.argv[2] # ... 동일한 로직 (프롬프트만 다름) system = ( 'You are a P&ID expert. Extract system tags only.\n' 'Return ONLY a JSON array.\n' '\n' 'System types to extract: FICQ, TICA, PICA, LICA, FIC, TIC, PIC, LIC\n' 'Format: [{"tagNo":"FICQ-10101","confidence":0.95},...]\n' ) # ... 나머지 로직 ``` #### 3.3.5 merge_results.py (결과 병합) ```python #!/usr/bin/env python3 """ 병렬 추출 결과 병합 스크립트 """ import json import sys import glob def main(): if len(sys.argv) != 3: print("Usage: python merge_results.py ") sys.exit(1) input_dir = sys.argv[1] output_file = sys.argv[2] # 모든 JSON 파일 읽기 all_tags = [] seen_tags = set() for filepath in glob.glob(f'{input_dir}/*_tags.json'): with open(filepath, 'r', encoding='utf-8') as f: tags = json.load(f) for tag in tags: tag_no = tag.get('tagNo') if tag_no and tag_no not in seen_tags: seen_tags.add(tag_no) all_tags.append(tag) # 결과 저장 with open(output_file, 'w', encoding='utf-8') as f: json.dump(all_tags, f, indent=2, ensure_ascii=False) print(f'총 추출 태그 수: {len(all_tags)}개') if __name__ == '__main__': main() ``` --- ## 4. 성능 예측 ### 4.1 Phase 1: 기하학적 추출 - **현재**: 1.4초 - **개선 후**: 1.4초 (변화 없음) ### 4.2 Phase 2: 위상 빌더 - **현재**: timeout (O(n²)) - **개선 후**: 2-3초 (R-tree O(n log n)) ### 4.3 Phase 3: 병렬 LLM 매핑 - **현재**: 예측 불가 (순차적 API 호출) - **개선 후**: 5-10초 (4개 프로그램 병렬 실행) **예상 속도 향상**: - Phase 2: 100배 이상 (timeout → 2-3초) - Phase 3: 3-5배 (순차적 → 병렬) ### 4.4 LINE (배관) 처리 특징 - **LINE이 20,868개 (72.4%)**로 압도적으로 많음 - **ezdxf로 기하학적 추출만으로 충분** (LLM 불필요) - **R-tree로 공간 인덱스 생성** → 배관 연결 - **GPU 자원 소모 없음** (CPU 전용 처리) **병렬 실행 구조**: ``` Phase 1: ezdxf로 28,000개 엔티티 추출 (1.4초) ├─ LINE (20,868개) → CPU 전용 추출 ├─ TEXT (3,562개) → CPU 전용 추출 └─ 기타 (3,589개) → CPU 전용 추출 Phase 2: R-tree로 위상 빌더 (2-3초) ├─ LINE 병합 → 배관 연결 └─ TEXT 병합 → 태그명 정제 Phase 3: 4개 프로그램 병렬 실행 (5-10초) ├─ pid_extractor_text.py (GPU 0) → 3,562개 태그 ├─ pid_extractor_valve.py (GPU 1) → 80개 태그 ├─ pid_extractor_equipment.py (GPU 2) → 50개 태그 └─ pid_extractor_system.py (GPU 3) → 120개 태그 ``` --- ## 5. GPU 자원 활용 전략 ### 5.1 vLLM의 GPU 할당 방식 **단일 프로세스 (현재 구조)**: ``` Python 프로세스 A ├─ LLM Request 1 → GPU 0 (100% 사용) ├─ LLM Request 2 → GPU 0 (대기) └─ LLM Request 3 → GPU 0 (대기) ``` → GPU 1, 2, 3은 놀고 있음 **다중 프로세스 (개선안)**: ``` Python 프로세스 A → GPU 0 (100% 사용) Python 프로세스 B → GPU 1 (100% 사용) Python 프로세스 C → GPU 2 (100% 사용) Python 프로세스 D → GPU 3 (100% 사용) ``` → 모든 GPU 카드를 최대한 활용 ### 5.2 병렬 실행 명령어 ```bash # 4개 프로그램을 동시에 실행 python pid_extractor_sensor.py /path/to/pid.dxf /path/to/output & python pid_extractor_valve.py /path/to/pid.dxf /path/to/output & python pid_extractor_equipment.py /path/to/pid.dxf /path/to/output & python pid_extractor_system.py /path/to/pid.dxf /path/to/output & # 모든 프로세스가 완료될 때까지 대기 wait # 결과 병합 python merge_results.py /path/to/output /path/to/output/merged_tags.json ``` --- ## 6. 구현 우선순위 | 순위 | 작업 | 예상 시간 | 영향도 | |------|------|-----------|--------| | 1 | R-tree 공간 인덱스 도입 | 1일 | HIGH | | 2 | pid_extractor_line.py 구현 (LINE 추출) | 0.5일 | HIGH | | 3 | pid_extractor_text.py 구현 (TEXT 추출) | 0.5일 | HIGH | | 4 | pid_extractor_valve.py 구현 | 0.5일 | HIGH | | 5 | pid_extractor_equipment.py 구현 | 0.5일 | HIGH | | 6 | pid_extractor_system.py 구현 | 0.5일 | HIGH | | 7 | merge_results.py 구현 | 0.5일 | LOW | | 8 | run_parallel_extract.sh 구현 | 0.5일 | LOW | | 9 | 테스트 및 벤치마크 | 0.5일 | LOW | **총 예상 시간**: 4.5일 **핵심 포인트**: - **LINE (20,868개)**: ezdxf로 기하학적 추출만으로 충분 (LLM 불필요) - **TEXT (3,562개)**: LLM 매핑 필요 (GPU 0) - **기타 (3,589개)**: LLM 매핑 필요 (GPU 1-3) --- ## 7. 결론 ### 7.1 핵심 개선 포인트 1. **Phase 1**: ezdxf로 28,000개 엔티티 추출 (1.4초) - **LINE (20,868개)**: 기하학적 추출만으로 충분 (LLM 불필요) - **TEXT (3,562개)**: LLM 매핑 필요 2. **Phase 2**: R-tree 공간 인덱스로 O(n²) → O(n log n) 개선 3. **Phase 3**: test_dxf_extract_pid*.py의 병렬 처리 구조 도입 4. **독립 프로그램 실행**: 각 청크를 별도의 Python 프로그램으로 실행 5. **GPU 자원 최대화**: 4개 프로그램이 각각 별도의 GPU에 할당 6. **LINE 처리 전략**: ezdxf + R-tree로 CPU 전용 처리 (GPU 불필요) ### 7.2 예상 성능 - **현재**: timeout (Phase 2에서 멈춤) - **개선 후**: 약 7-13초 (28,000개 엔티티 기준) - **속도 향상**: 100배 이상 (Phase 2), 3-5배 (Phase 3) ### 7.3 구현 전략 1. 먼저 Phase 2 (공간 인덱스) 구현 → Phase 2 timeout 해결 2. Phase 3 (병렬 LLM) 구현 → test_dxf_extract_pid*.py 구조 참고 3. 전체 파이프라인 통합 → 벤치마크 테스트 ### 7.4 GPU 활용 전략 - **4개의 독립된 Python 프로그램**을 동시에 실행 - 각 프로그램이 vLLM의 별도 GPU에 할당됨 - **모든 GPU 카드를 100% 활용**하여 처리 속도 최대화