Files
ExperionCrawler/P&ID_병렬LLM_아키텍처_개선안.md

391 lines
13 KiB
Markdown

# P&ID 도면 파싱 병렬 LLM 아키텍처 개선안
## 1. 기존 문제점 분석
### 1.1 현재 구조의 병목
| 단계 | 문제점 | 심각도 |
|------|--------|--------|
| 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...'
}
]
# 각 청크를 순차적으로 처리하지만, LLM은 병렬로 실행 가능
for chunk in chunks:
resp = llm.chat.completions.create(...)
```
**핵심 발견**:
- **청크 단위 분할**: 태그 유형별로 프롬프트를 분리
- **동시 실행 가능**: 각 청크는 독립적이므로 병렬 실행 가능
- **LLM 자원 최대화**: vLLM의 tensor parallelism 활용 가능
---
## 2. 병렬 LLM 처리 아키텍처 설계
### 2.1 전체 파이프라인 구조 (개선안)
```
┌─────────────────────────────────────────────────────────────────────┐
│ P&ID 도면 파싱 파이프라인 (병렬 LLM) │
└─────────────────────────────────────────────────────────────────────┘
Phase 1: 기하학적 추출 (ezdxf)
├─ DXF 파일 로드 (0.84초)
├─ 엔티티별 BBox 계산 (0.58초)
└─ 결과: 28,257개 GeometricEntity
Phase 2: 위상 빌더 (공간 인덱스 + 병렬 LLM)
├─ 공간 인덱스 생성 (R-tree)
├─ 노드 병합 (O(n log n))
└─ 결과: NetworkX 그래프
Phase 3: 지능형 매핑 (병렬 LLM)
├─ 태그 유형별 청크 분할
│ ├─ Sensor Tags (FT, FIT, LT, PT, TE, ...)
│ ├─ Valve Tags (FCV, TCV, LCV, PCV, XV, ...)
│ ├─ Equipment Tags (Pump, Tank, Heat Exchanger)
│ └─ System Tags (FICQ, TICA, PICA, ...)
├─ 병렬 LLM 실행 (4개 청크 동시에)
│ ├─ LLM Worker 1: Sensor Tags → 100개 태그
│ ├─ LLM Worker 2: Valve Tags → 80개 태그
│ ├─ LLM Worker 3: Equipment Tags → 50개 태그
│ └─ LLM Worker 4: System Tags → 120개 태그
└─ 결과: 350개 매핑된 태그
```
### 2.2 병렬 LLM 워커 구조
```python
# 병렬 LLM 워커
import asyncio
from typing import List, Dict, Any
from openai import AsyncOpenAI
class ParallelLLMWorker:
def __init__(self, api_client: AsyncOpenAI, max_concurrent: int = 4):
self.client = api_client
self.max_concurrent = max_concurrent
self.semaphore = asyncio.Semaphore(max_concurrent)
async def process_chunk(self, chunk: Dict[str, Any]) -> List[Dict[str, Any]]:
"""단일 청크 처리 (비동기 + 세마포어로 병렬 제한)"""
async with self.semaphore:
system = chunk['system']
user = chunk['user'].format(text=chunk['text'])
response = await self.client.chat.completions.create(
model='Qwen/Qwen3-Coder-Next-FP8',
messages=[
{'role': 'system', 'content': system},
{'role': 'user', 'content': user},
],
max_tokens=65536,
temperature=0.1,
)
return self._parse_response(response)
async def process_all_chunks(self, chunks: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""모든 청크 병렬 처리"""
tasks = [self.process_chunk(chunk) for chunk in chunks]
results = await asyncio.gather(*tasks)
# 결과 병합
all_tags = []
seen_tags = set()
for tags in results:
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)
return all_tags
```
---
## 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 매핑 (신규 구현)
```python
# pid_parallel_llm_mapper.py (신규 파일)
import asyncio
from typing import List, Dict, Any
from openai import AsyncOpenAI
from rapidfuzz import process, fuzz
class ParallelLLMMapper:
def __init__(self, graph, system_tags: List[str], api_client: AsyncOpenAI,
max_concurrent: int = 4):
self.graph = graph
self.system_tags = system_tags
self.client = api_client
self.max_concurrent = max_concurrent
self.semaphore = asyncio.Semaphore(max_concurrent)
def create_chunks(self, node_ids: List[str]) -> List[Dict[str, Any]]:
"""노드를 태그 유형별로 청크 분할"""
# 태그 유형별 분류
sensors = []
valves = []
equipment = []
system = []
for node_id in node_ids:
node_data = self.graph.nodes[node_id]
tag_text = node_data.get('value', '').upper()
# 태그 유형에 따라 분류
if any(x in tag_text for x in ['FT', 'FIT', 'LT', 'PT', 'TE', 'PG', 'LG', 'TG']):
sensors.append(node_id)
elif any(x in tag_text for x in ['FCV', 'TCV', 'LCV', 'PCV', 'XV', 'FV', 'LV', 'PV', 'TV']):
valves.append(node_id)
elif any(x in tag_text for x in ['PUMP', 'TANK', 'HEAT', 'EXCHANGER']):
equipment.append(node_id)
else:
system.append(node_id)
# 청크 생성
chunks = []
if sensors:
chunks.append({
'name': 'Sensors',
'node_ids': sensors,
'system': 'You are a P&ID expert. Extract sensor tags only.',
'user': 'Extract sensor tags: {tags}'
})
if valves:
chunks.append({
'name': 'Valves',
'node_ids': valves,
'system': 'You are a P&ID expert. Extract valve tags only.',
'user': 'Extract valve tags: {tags}'
})
if equipment:
chunks.append({
'name': 'Equipment',
'node_ids': equipment,
'system': 'You are a P&ID expert. Extract equipment tags only.',
'user': 'Extract equipment tags: {tags}'
})
if system:
chunks.append({
'name': 'System',
'node_ids': system,
'system': 'You are a P&ID expert. Extract system tags only.',
'user': 'Extract system tags: {tags}'
})
return chunks
async def process_chunk(self, chunk: Dict[str, Any]) -> Dict[str, Any]:
"""단일 청크 처리 (비동기 + 세마포어)"""
async with self.semaphore:
node_ids = chunk['node_ids']
tag_texts = [self.graph.nodes[nid]['value'] for nid in node_ids]
# RapidFuzz 후보 추출
candidates_list = []
for tag_text in tag_texts:
candidates = process.extract(tag_text, self.system_tags, limit=5)
candidates_list.append(candidates)
# LLM 프롬프트 생성
prompt = f"""
{chunk['system']}
다음 태그들을 시스템 태그와 매핑하세요:
{chr(10).join(f'{t} -> {c}' for t, c in zip(tag_texts, candidates_list))}
JSON 형식으로 응답:
{{"node_id": "resolved_tag", ...}}
"""
response = await self.client.chat.completions.create(
model='Qwen/Qwen3-Coder-Next-FP8',
messages=[{'role': 'user', 'content': prompt}],
max_tokens=65536,
temperature=0.1,
)
return self._parse_response(response)
async def process_all_chunks(self, chunks: List[Dict[str, Any]]) -> Dict[str, Any]:
"""모든 청크 병렬 처리"""
tasks = [self.process_chunk(chunk) for chunk in chunks]
results = await asyncio.gather(*tasks)
# 결과 병합
merged = {}
for result in results:
merged.update(result)
return merged
```
---
## 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배 (순차적 → 병렬)
---
## 5. 구현 우선순위
| 순위 | 작업 | 예상 시간 | 영향도 |
|------|------|-----------|--------|
| 1 | R-tree 공간 인덱스 도입 | 1일 | HIGH |
| 2 | 병렬 LLM 워커 구현 | 1일 | HIGH |
| 3 | Phase 2-3 통합 | 0.5일 | MEDIUM |
| 4 | 테스트 및 벤치마크 | 0.5일 | LOW |
**총 예상 시간**: 3일
---
## 6. 참고: test_dxf_extract_pid*.py의 성공 요인
### 6.1 청크 단위 분할
- 태그 유형별로 프롬프트를 분리하여 **의도적 병렬화** 가능
- 각 청크는 독립적이므로 **실패 격리** 가능
### 6.2 vLLM의 tensor parallelism 활용
- `Qwen/Qwen3-Coder-Next-FP8` 모델은 **8개 GPU 카드**에 분산 실행 가능
- 4개 청크를 동시에 실행하면 **모든 GPU 카드를 최대한 활용**
### 6.3 비동기 처리
- `asyncio.gather()`로 여러 청크를 동시에 실행
- 각 청크는 `async with semaphore`로 병렬도 제한
---
## 7. 결론
### 7.1 핵심 개선 포인트
1. **Phase 2**: R-tree 공간 인덱스로 O(n²) → O(n log n) 개선
2. **Phase 3**: test_dxf_extract_pid*.py의 병렬 처리 구조 도입
3. **병렬 LLM**: 4개 청크를 동시에 실행하여 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. 전체 파이프라인 통합 → 벤치마크 테스트