๐ [Phase 2] Uni-HAR: On-device Optimization Log
From 221 GFLOPs Bottleneck to MPU-Deployable Ultra-Lightweight Inference
1. The Bottleneck: ์ง๋ฅ์ ๋๊ฐ (The Cost of Intelligence)
Phase 1 ์ฐ๊ตฌ๋ฅผ ํตํด ๋ฒ์ฉ์ ์ด๊ณ ๊ฐ๊ฑดํ ํ๋ฅ ๋ถํฌ ๊ธฐ๋ฐ์ HAR ๋ชจ๋ธ(Uni-HAR)์ ๊ตฌ์ถํ๋ ๋ฐ ์ฑ๊ณตํ์ต๋๋ค. ํ์ง๋ง, ๋ชจ๋ธ์ ์ง๋ฅ(ํํ๋ ฅ)์ ๊ทน๋ํํ ๋๊ฐ๋ ํ์ค ์ธ๊ณ์ ํ๋์จ์ด ํ๊ณ๋ผ๋ ๋ฒฝ์ผ๋ก ๋์์์ต๋๋ค.
ํ๋ถ ์บก์คํค ์์ (Phase 0)์ 1D-CNN๊ณผ ํ์ฌ(Phase 1)์ Uni-HAR ๋ชจ๋ธ ์ฐ์ฐ๋์ fvcore ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ์ง์ ํ๋กํ์ผ๋งํ ๊ฒฐ๊ณผ๋ ๊ทน๋ช
ํ์ต๋๋ค.
[Phase 0] ๊ธฐ์กด 1D-CNN ๋ชจ๋ธ (ํฌ์ฆ ๋จ์ผ ๋ชจ๋ฌ)
Total MFLOPs: 0.5099 MFLOPs
-> ์ด๊ฒฝ๋์ด์ง๋ง ์์ธก์ด ๋ถ์์ ํ๊ณ ๋ฏธ์ธํ ๋์ ๊ตฌ๋ถ์ด ๋ถ๊ฐ๋ฅ.
[Phase 1] Uni-HAR ๋ชจ๋ธ (Multimodal, Factorized Transformer)
Total FLOPs: 221,567,666,944 FLOPs
Total GFLOPs: 221.5677 GFLOPs (Parameters: 14.2M)
[Major Component FLOPs Breakdown]
- Image Encoder: 218.23 GFLOPs (98.5%)
- Pose Backbone: 3.32 GFLOPs (1.5%)
-> ํ๋กํ์ผ๋ง ์์ธ ๊ฒฐ๊ณผ ํ์ธ
๋ฌธ์ ์ธ์: 221 GFLOPs๋ ์๋ฒ๊ธ GPU์์๋ ๋ฌด๋ฆฌ๊ฐ ์์ง๋ง, ํ๊ฒ ํ๊ฒฝ์ธ ์ ํ๋ Edge Device(Jetson, MPU ๋ฑ)์์๋ Out-of-Memory(OOM)์ ์ค์๊ฐ ์ฒ๋ฆฌ ๋ถ๊ฐ(Frame Drop) ๋ฅผ ์ ๋ฐํ๋ ์น๋ช ์ ์ธ ์์น์์ต๋๋ค.
์ด์ 1์ฐจ ๋ชจ๋ธ์ ๋์ ์ง๋ฅ(Representation Power)์ ์ ์งํ ์ฑ, ์ด๋ฅผ ์ฃ์ง ๋๋ฐ์ด์ค์ ์ฌ๋ฆฌ๊ธฐ ์ํด ๊ธฐ์กด ๋ชจ๋ธ์ forward ๊ตฌ์กฐ๋ฅผ ํด์ฒด(Decoupling)ํ๊ณ **์ธ ๊ฐ์ง ํ๋์จ์ด ์นํ์ ์ต์ ํ(Hardware-Aware Optimization)**๋ฅผ ์ ์ฉํ StreamingUniHAR ์ํคํ
์ฒ๋ฅผ ์๋กญ๊ฒ ์ค๊ณํ์ต๋๋ค.
2. Optimization Step 1: Feature Caching (Stream-Aware Inference)
"Vision Domain์ LLM์ KV Cache ์๋ฆฌ๋ฅผ ์ด์ํ๋ค"
์ ์ฒด ์ฐ์ฐ์ 98.5% (218 GFLOPs) ๊ฐ Image Encoder(ResNet18)์์ ๋ฐ์ํ์ต๋๋ค. ์ด๋ 120ํ๋ ์ ๋จ์์ ์ฌ๋ผ์ด๋ฉ ์๋์ฐ ์ถ๋ก ์, ์๋์ฐ๊ฐ 1ํ๋ ์ ์ด๋ํ ๋๋ง๋ค ๊ณผ๊ฑฐ์ 119ํ๋ ์์ ๋ํ RGB ํน์ง์ ๋ฌด์๋ฏธํ๊ฒ ์ค๋ณต ์ฐ์ฐ(Coupled Forward) ํ๊ณ ์์๊ธฐ ๋๋ฌธ์ ๋๋ค.
ํด๊ฒฐ ๋ฐฉ์ (Architecture Decoupling): Autoregressive LLM์ด ์ด์ ํ ํฐ ์ฐ์ฐ์ ๋ฐ๋ณตํ์ง ์๊ธฐ ์ํด ์ฌ์ฉํ๋ KV Cache ๊ฐ๋ ์ Vision ํ์ดํ๋ผ์ธ์ ์ด์ํ์ต๋๋ค. ํต์ง ๋ชจ๋ธ ๊ตฌ์กฐ๋ฅผ ํ๋ ์ ๋จ์์ Feature Extractor์ Temporal Aggregator๋ก ๋ถ๋ฆฌํ์ต๋๋ค.
# StreamingUniHAR์ ํต์ฌ ๋ก์ง: ํ(Queue) ๋กค๋ง ๊ธฐ๋ฐ ์บ์ฑ
# ํ๋ฅผ ํ ์นธ์ฉ ๋ฐ๊ณ ์ต์ ํน์ง๊ฐ์ ๋งจ ๋ค์ ์ฝ์
(1ํ๋ ์๋ง ์ฐ์ฐ)
self.pose_raw_cache = torch.roll(self.pose_raw_cache, shifts=-1, dims=1)
self.pose_raw_cache[:, -1:, :, :] = curr_pose
self.img_feat_cache = torch.roll(self.img_feat_cache, shifts=-1, dims=1)
self.img_feat_cache[:, -1:, :] = curr_img_feat
- ๊ฒฐ๊ณผ: ์๋์ฐ ์ ์ฒด๋ฅผ ํ ๋ฒ์ ์ฐ์ฐํ์ง ์๊ณ , ์๋ก ๋ค์ด์ค๋ 1๊ฐ์ ํ๋ ์์ ๋ํด์๋ง Image Encoder ์ฐ์ฐ์ ์ํํ๋๋ก ๋ณ๊ฒฝํ์ฌ ํ๋ ์๋น ์๊ฐ ์ฐ์ฐ๋์ 221 GFLOPs์์ ์ฝ 1.8 GFLOPs ์์ค์ผ๋ก ํํํ(Smoothing) ํ์ต๋๋ค.
3. Optimization Step 2: Temporal Sparsity (Dynamic Skipping)
"๊ฐ๋ฒผ์ด ๋ชจ๋ฌ๋ฆฌํฐ๋ก ๋ฌด๊ฑฐ์ด ๋ชจ๋ฌ๋ฆฌํฐ์ ์ฐ์ฐ์ ํต์ ํ๋ค (Cross-modal Sparsity)"
Image Encoder์ ์ค๋ณต ์ฐ์ฐ์ ์บ์ฑ์ผ๋ก ๋ง์์ง๋ง, ์ฃ์ง ํ๊ฒฝ์ ์นด๋ฉ๋ผ๋ ํผ์ฌ์ฒด๊ฐ ์ ์งํด ์์ ๋๋ ๊ณ์ ๋์๊ฐ๋๋ค. ํ๋ ์ธ์ ๋ฐ์ดํฐ์ ๋ถ์ ๊ฒฐ๊ณผ, ์ธ์ฒด๊ฐ ๋ฉ์ถฐ ์๋ ๊ตฌ๊ฐ(Redundancy)์ด ๊ฝค ๋ง์์ ํ์ธํ์ต๋๋ค.
ํด๊ฒฐ ๋ฐฉ์: ๊ฐ๋ฒผ์ด ์ฐ์ฐ๋(1.5%)์ ๊ฐ์ง Pose ๋ฐ์ดํฐ๋ฅผ ํ์ฉํด ๋ฌด๊ฑฐ์ด ์ฐ์ฐ๋(98.5%)์ ๊ฐ์ง RGB CNN ์ฐ์ฐ์ ์คํตํ๋ ๊ตฌ์กฐ๋ฅผ ์ค๊ณํ์ต๋๋ค. ํ๋ ์ ๊ฐ ํฌ์ฆ ํน์ง ๋ฒกํฐ์ ๋ณํ๋()์ด ํน์ Threshold ์ดํ์ด๋ฉด, ๋ฌด๊ฑฐ์ด CNN ์ถ์ถ ์์ฒด๋ฅผ ์๋ต(Zero-Compute) ํ๊ณ ์ด์ ํ๋ ์์ ํน์ง์ ๊ทธ๋๋ก ๋ณต์ฌํฉ๋๋ค.
# [Optimization] Temporal Sparsity Check
# ํ์ฌ ํฌ์ฆ์ ์ด์ ํฌ์ฆ์ ๋ณํ๋(delta) ๊ณ์ฐ
delta = torch.norm(curr_pose - self.last_pose, p=2, dim=-1).mean().item()
if delta < self.threshold and self.last_img_feat is not None:
# ์์ง์์ด ์๊ณ์น ์ดํ -> ๋ฌด๊ฑฐ์ด CNN ์ถ์ถ ๊ณผ์ ์๋ต (Zero-Compute)
curr_img_feat = self.last_img_feat
is_sparse = True
else:
# ์์ง์ ๋ฐ์ -> ํ์ฌ ํ๋ ์๋ง CNN ์ฐ์ฐ ์คํ
curr_img_4d = curr_img.view(B, 3, curr_img.shape[-2], curr_img.shape[-1])
curr_img_feat = self.image_encoder(curr_img_4d).unsqueeze(1)
- ๊ฒฐ๊ณผ: ํ๊ท ์ ์ธ ์ถ๋ก ์๋๋ฆฌ์ค์์ ํ๋ ๋ณํ๊ฐ ์๋ ๊ตฌ๊ฐ์ RGB ์ธ์ฝ๋ฉ ์ฐ์ฐ์ ์์ ํ ์ฐจ๋จํ์ฌ, ์ ์ฒด ์ฐ์ฐ๋์ ์ถ๊ฐ๋ก 30~40% ์ ๊ฐํ์ต๋๋ค.
4. Optimization Step 3: 1.58-bit QAT (BitNet for Vision)
"MPU ํ์ฌ๋ฅผ ์ํ ๊ทน๋จ์ ์์ํ์ Adder-only ์ฐ์ฐ"
์ด๋ฏธ์ง ์ธ์ฝ๋ ๋ณ๋ชฉ์ ํด๊ฒฐํ ํ, ๋ง์ง๋ง ํ๊ฒ์ 3.3 GFLOPs๋ฅผ ์ฐจ์งํ๋ Pose Backbone (Factorized Spatio-Temporal Transformer) ์ด์์ต๋๋ค. MPU(Microprocessor Unit) ๋ ๋ฒจ์์๋ FP32 ์ ๋ฐ๋์ MAC(Multiply-Accumulate) ํ๋ ฌ ๊ณฑ ์ฐ์ฐ ์์ฒด๊ฐ ์ ๋ ฅ๊ณผ ๋์ญํญ์ ํ๊ณ์ ๋ถ๋ชํ๋๋ค.
ํด๊ฒฐ ๋ฐฉ์:
์ต์ LLM ๊ฒฝ๋ํ ํธ๋ ๋์ธ BitNet b1.58์ ๊ทน๋จ์ ์์ํ ๊ธฐ๋ฒ์ Transformer ๊ธฐ๋ฐ ๋น์ ๋ชจ๋ธ์ ์ ์ฉํ๋ ์คํ์ ์งํ ์ค์
๋๋ค.
class WeightQuantSTE(torch.autograd.Function):
@staticmethod
def forward(ctx, weight):
scale = weight.abs().mean().clamp(min=1e-8)
quantized = torch.round(weight / scale).clamp(-1, 1) # {-1, 0, 1} ๋งคํ
ctx.save_for_backward(weight)
return quantized * scale
class BitLinear(nn.Module):
"""๊ธฐ์กด nn.Linear๋ฅผ ๋์ฒดํ๋ 1.58-bit ๋ ์ด์ด"""
def __init__(self, in_features, out_features, bias=True):
super().__init__()
self.weight = nn.Parameter(torch.randn(out_features, in_features) * 0.02)
def forward(self, x):
q_weight = WeightQuantSTE.apply(self.weight)
return F.linear(x, q_weight, None)
- QAT (Quantization-Aware Training): ๋ฏธ๋ถ ๋ถ๊ฐ๋ฅํ ์์ํ ํจ์์ ์ญ์ ํ(Backpropagation)๋ฅผ ์ํด STE(Straight-Through Estimator) ๊ธฐ๋ฐ์ Custom Autograd๋ฅผ ๊ตฌํํ์ฌ ๊ธฐ์กด MLP์
nn.Linear๋ฅผBitLinear๋ก ๊ต์ฒดํ์ต๋๋ค. - ํ๋์จ์ด ๊ด์ ์ ์ด์ : ALU(์ฐ์ ๋ ผ๋ฆฌ์ฐ์ฐ์ฅ์น) ๋ ๋ฒจ์์ ๋ฌด๊ฑฐ์ด Multiplier(๊ณฑ์ ๊ธฐ)๋ฅผ ๋จ์ Adder(๊ฐ์ฐ๊ธฐ)๋ก ์นํ. ํ๋ผ๋ฏธํฐ ๋ฉ๋ชจ๋ฆฌ ์ฉ๋์ ์์ถํ์ฌ DRAM ์ ๊ทผ์ ์ต์ํํ๊ณ SRAM ๋ด๋ถ์์ ์ฐ์ฐ์ด ์๋ฃ๋ ์ ์๋ ํ ๋๋ฅผ ๋ง๋ จํ์ต๋๋ค.
5. Conclusion & Next Steps
Phase 2์ ์ต์ ํ ํ์ดํ๋ผ์ธ ์ค๊ณ๋ฅผ ํตํด, Uni-HAR๋ '์ฐ๊ตฌ์ค์ ๋ฌด๊ฑฐ์ด ๋ชจ๋ธ'์์ 'ํ์ฅ์ ์ฃ์ง ๋๋ฐ์ด์ค(MPU)์์ ์ค์๊ฐ ๋์ ๊ฐ๋ฅํ ์์ง' ์ผ๋ก ๋ณ๋ชจํ ์ค๋น๋ฅผ ๋ง์ณค์ต๋๋ค.
[Summary of Transformation]
- Phase 0: ๊ฐ๋ณ์ง๋ง ๋ฉ์ฒญํ๋ค. (0.5 MFLOPs, 1D-CNN)
- Phase 1: ๋๋ํ์ง๋ง ๋ฌด๊ฒ๋ค. (221 GFLOPs, MPU OOM ๋ฐ์)
- Phase 2: ๋๋ํจ์ ์ ์งํ๋ฉฐ ๊ฐ๋ฒผ์์ก๋ค. (Feature Caching & Sparsity๋ก ํ๋ ์๋น ์ฐ์ฐ 99% ์ญ๊ฐ, 1.58-bit QAT๋ก Transformer ์ฐ์ฐ ํจ์จํ)
[Future Work: Heterogeneous Dispatching]
๋ค์์ผ๋ก ์งํํ๊ณ ์ ํ๋ ์ฃผ์ ๋ ์ด๊ธฐ์ข
ํ๋์จ์ด(CPU/GPU/NPU) ์๋ ํ ๋น ๋ผ์ฐํ
์
๋๋ค.
๊ฐ๋ฒผ์ด ์ ์ด ๋ก์ง(Sparsity ํ๋จ, Buffer ๋กค๋ง)์ CPU๊ฐ ์ ๋ดํ๊ณ , Feature Extraction๊ณผ QAT ๋ณ๋ ฌ ํ๋ ฌ ์ฐ์ฐ์ Jetson Board๋ฑ์ GPU/NPU๋ก Dispatching ํ์ฌ ์นฉ์
์ ์ปดํจํ
๋ฆฌ์์ค ๋ญ๋น๋ฅผ 0%๋ก ๋ง๋๋ ํตํฉ ์์ง ์ต์ ํ๋ฅผ ์ด์ด๋๊ฐ ์์ ์
๋๋ค.