๋ฉํฐ ๋ธ๋ก ์กฐ์ ๊ธฐ์ด
๊ฐ์
์ฒซ ๋ฒ์งธ ํด๋ฌ์คํฐ ํ๋ก๊ทธ๋๋ฐ ๋์ ์ ์ค์ ๊ฒ์ ํ์ํฉ๋๋ค! ์ด ์น์ ์์๋ SM90+ ํด๋ฌ์คํฐ API๋ฅผ ์ฌ์ฉํ ๋ธ๋ก ๊ฐ ์กฐ์ ์ ๊ธฐ๋ณธ ๊ตฌ์ฑ ์์๋ฅผ ์๊ฐํฉ๋๋ค.
๋์ ๊ณผ์ : 4๊ฐ์ ์ค๋ ๋ ๋ธ๋ก์ด ์กฐ์ ํ์ฌ ์๋ก ๋ค๋ฅธ ๋ฐ์ดํฐ ๋ฒ์๋ฅผ ์ฒ๋ฆฌํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๊ณต์ ์ถ๋ ฅ ๋ฐฐ์ด์ ์ ์ฅํ๋ ๋ฉํฐ ๋ธ๋ก ํ์คํ ๊ทธ๋จ ์๊ณ ๋ฆฌ์ฆ์ ๊ตฌํํฉ๋๋ค.
ํต์ฌ ํ์ต: cluster_arrive() โ ์ฒ๋ฆฌ โ cluster_wait()๋ผ๋ ํ์์ ์ธ ํด๋ฌ์คํฐ ๋๊ธฐํ ํจํด์ ๋ฐฐ์๋๋ค. Puzzle 29์ barrier()์์ ๋ฐฐ์ด ๋๊ธฐํ ๊ฐ๋
์ ํ์ฅํฉ๋๋ค.
๋ฌธ์ : ๋ฉํฐ ๋ธ๋ก ํ์คํ ๊ทธ๋จ ๊ตฌ๊ฐ ๋ถ๋ฅ
Puzzle 27๊ณผ ๊ฐ์ ๊ธฐ์กด์ ๋จ์ผ ๋ธ๋ก ์๊ณ ๋ฆฌ์ฆ์ ํ๋์ ๋ธ๋ก์ด ๊ฐ์ง ์ค๋ ๋ ์ฉ๋(์: 256๊ฐ ์ค๋ ๋) ๋ด์ ๋ค์ด์ค๋ ๋ฐ์ดํฐ๋ง ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. Puzzle 8์ ๊ณต์ ๋ฉ๋ชจ๋ฆฌ ์ฉ๋์ ์ด๊ณผํ๋ ๋ ํฐ ๋ฐ์ดํฐ์ ์ ๊ฒฝ์ฐ, ์ฌ๋ฌ ๋ธ๋ก์ด ํ๋ ฅํด์ผ ํฉ๋๋ค.
๊ณผ์ : 4๊ฐ ๋ธ๋ก ๊ฐ๊ฐ์ด ์๋ก ๋ค๋ฅธ ๋ฐ์ดํฐ ๋ฒ์๋ฅผ ์ฒ๋ฆฌํ๊ณ , ๊ณ ์ ํ ๋ธ๋ก ์์๋ก ๊ฐ์ ์ค์ผ์ผ๋งํ๋ฉฐ, Puzzle 29์ ๋๊ธฐํ ํจํด์ ์ฌ์ฉํ์ฌ ๋ค๋ฅธ ๋ธ๋ก๋ค๊ณผ ์กฐ์ ํจ์ผ๋ก์จ ๋ชจ๋ ๋ธ๋ก์ ์ฒ๋ฆฌ๊ฐ ์๋ฃ๋ ํ์์ผ ์ต์ข ๊ฒฐ๊ณผ๋ฅผ ์ฝ์ ์ ์๋๋ก ํ๋ ํ์คํ ๊ทธ๋จ์ ๊ตฌํํ์ธ์.
๋ฌธ์ ๋ช ์ธ
๋ฉํฐ ๋ธ๋ก ๋ฐ์ดํฐ ๋ถ๋ฐฐ:
- Block 0: ์์ 0-255๋ฅผ ์ฒ๋ฆฌ, 1๋ฐฐ ์ค์ผ์ผ๋ง
- Block 1: ์์ 256-511์ ์ฒ๋ฆฌ, 2๋ฐฐ ์ค์ผ์ผ๋ง
- Block 2: ์์ 512-767์ ์ฒ๋ฆฌ, 3๋ฐฐ ์ค์ผ์ผ๋ง
- Block 3: ์์ 768-1023์ ์ฒ๋ฆฌ, 4๋ฐฐ ์ค์ผ์ผ๋ง
์กฐ์ ์๊ตฌ์ฌํญ:
- ๊ฐ ๋ธ๋ก์
cluster_arrive()๋ฅผ ์ฌ์ฉํ์ฌ ์๋ฃ๋ฅผ ์๋ ค์ผ ํฉ๋๋ค - ๋ชจ๋ ๋ธ๋ก์
cluster_wait()๋ฅผ ์ฌ์ฉํ์ฌ ๋ค๋ฅธ ๋ธ๋ก์ ๊ธฐ๋ค๋ ค์ผ ํฉ๋๋ค - ์ต์ข ์ถ๋ ฅ์ ๊ฐ ๋ธ๋ก์ ์ฒ๋ฆฌ๋ ํฉ๊ณ๋ฅผ 4๊ฐ ์์ ๋ฐฐ์ด๋ก ๋ณด์ฌ์ค๋๋ค
์ค์
- ๋ฌธ์ ํฌ๊ธฐ:
SIZE = 1024์์ (1D ๋ฐฐ์ด) - ๋ธ๋ก ์ค์ :
TPB = 256๋ธ๋ก๋น ์ค๋ ๋ ์(256, 1) - ๊ทธ๋ฆฌ๋ ์ค์ :
CLUSTER_SIZE = 4ํด๋ฌ์คํฐ๋น ๋ธ๋ก ์(4, 1) - ๋ฐ์ดํฐ ํ์
:
DType.float32 - ๋ฉ๋ชจ๋ฆฌ ๋ ์ด์์: ์
๋ ฅ
Layout.row_major(SIZE), ์ถ๋ ฅLayout.row_major(CLUSTER_SIZE)
์ค๋ ๋ ๋ธ๋ก ๋ถ๋ฐฐ:
- Block 0: ์ค๋ ๋ 0-255 โ ์์ 0-255
- Block 1: ์ค๋ ๋ 0-255 โ ์์ 256-511
- Block 2: ์ค๋ ๋ 0-255 โ ์์ 512-767
- Block 3: ์ค๋ ๋ 0-255 โ ์์ 768-1023
์์ฑํ ์ฝ๋
fn cluster_coordination_basics[
in_layout: Layout, out_layout: Layout, tpb: Int
](
output: LayoutTensor[dtype, out_layout, MutAnyOrigin],
input: LayoutTensor[dtype, in_layout, ImmutAnyOrigin],
size: Int,
):
"""Real cluster coordination using SM90+ cluster APIs."""
global_i = Int(block_dim.x * block_idx.x + thread_idx.x)
local_i = thread_idx.x
# Check what's happening with cluster ranks
my_block_rank = Int(block_rank_in_cluster())
block_id = Int(block_idx.x)
shared_data = LayoutTensor[
dtype,
Layout.row_major(tpb),
MutAnyOrigin,
address_space = AddressSpace.SHARED,
].stack_allocation()
# FIX: Use block_idx.x for data distribution instead of cluster rank
# Each block should process different portions of the data
var data_scale = Float32(
block_id + 1
) # Use block_idx instead of cluster rank
# Phase 1: Each block processes its portion
if global_i < size:
shared_data[local_i] = input[global_i] * data_scale
else:
shared_data[local_i] = 0.0
barrier()
# Phase 2: Use cluster_arrive() for inter-block coordination
# Signal this block has completed processing
# FILL IN 1 line here
# Block-level aggregation (only thread 0)
if local_i == 0:
# FILL IN 4 line here
...
# Wait for all blocks in cluster to complete
# FILL IN 1 line here
์ ์ฒด ํ์ผ ๋ณด๊ธฐ: problems/p34/p34.mojo
ํ
๋ธ๋ก ์๋ณ ํจํด
block_rank_in_cluster()๋ฅผ ์ฌ์ฉํ์ฌ ํด๋ฌ์คํฐ ์์(0-3)๋ฅผ ์ป์ต๋๋ค- ๊ทธ๋ฆฌ๋ ์คํ์์ ์์ ์ ์ธ ๋ธ๋ก ์ธ๋ฑ์ฑ์ ์ํด
Int(block_idx.x)๋ฅผ ์ฌ์ฉํฉ๋๋ค - ๋ธ๋ก ์์น์ ๋ฐ๋ผ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๋ฅผ ์ค์ผ์ผ๋งํ์ฌ ๊ณ ์ ํ ๊ฒฐ๊ณผ๋ฅผ ๋ง๋ญ๋๋ค
๊ณต์ ๋ฉ๋ชจ๋ฆฌ ์กฐ์
LayoutTensor[dtype, Layout.row_major(tpb), MutAnyOrigin, address_space = AddressSpace.SHARED].stack_allocation()์ผ๋ก ๊ณต์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ํ ๋นํฉ๋๋ค (Puzzle 8์ ๊ณต์ ๋ฉ๋ชจ๋ฆฌ ๊ธฐ์ด ์ฐธ๊ณ )block_id + 1๋ก ์ค์ผ์ผ๋งํ์ฌ ๋ธ๋ก๋ง๋ค ๊ณ ์ ํ ์ค์ผ์ผ๋ง์ ์ ์ฉํฉ๋๋ค- ์ ๋ ฅ ๋ฐ์ดํฐ ์ ๊ทผ ์ ๊ฒฝ๊ณ ๊ฒ์ฌ๋ฅผ ์ฌ์ฉํฉ๋๋ค (Puzzle 3์ ๊ฐ๋ ํจํด)
ํด๋ฌ์คํฐ ๋๊ธฐํ ํจํด
- ์ฒ๋ฆฌ: ๊ฐ ๋ธ๋ก์ด ์์ ์ ๋ฐ์ดํฐ ์์ญ์ ์ฒ๋ฆฌํฉ๋๋ค
- ์ ํธ:
cluster_arrive()๋ก ์ฒ๋ฆฌ ์๋ฃ๋ฅผ ์๋ฆฝ๋๋ค - ์ฐ์ฐ: ๋ธ๋ก ๋ด๋ถ ์ฐ์ฐ (๋ฆฌ๋์ , ์ง๊ณ)
- ๋๊ธฐ:
cluster_wait()๋ก ๋ชจ๋ ๋ธ๋ก์ด ์๋ฃ๋ ๋๊น์ง ๋๊ธฐํฉ๋๋ค
๋ธ๋ก ๋ด๋ถ ์ค๋ ๋ ์กฐ์
- ํด๋ฌ์คํฐ ์ฐ์ฐ ์ ์ ๋ธ๋ก ๋ด๋ถ ๋๊ธฐํ๋ฅผ ์ํด
barrier()๋ฅผ ์ฌ์ฉํฉ๋๋ค (Puzzle 29์ ๋ฐฐ๋ฆฌ์ด ๊ฐ๋ ) - ์ค๋ ๋ 0๋ง ์ต์ข ๋ธ๋ก ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋กํด์ผ ํฉ๋๋ค (๋ธ๋ก ํ๋ก๊ทธ๋๋ฐ์ ๋จ์ผ ์ฐ๊ธฐ ํจํด)
- ์์ ์ ์ธ ์ธ๋ฑ์ฑ์ ์ํด ๊ฒฐ๊ณผ๋ฅผ
output[block_id]์ ์ ์ฅํฉ๋๋ค
์ฝ๋ ์คํ
pixi run p34 --coordination
uv run poe p34 --coordination
์์ ์ถ๋ ฅ:
Testing Multi-Block Coordination
SIZE: 1024 TPB: 256 CLUSTER_SIZE: 4
Block coordination results:
Block 0 : 127.5
Block 1 : 255.0
Block 2 : 382.5
Block 3 : 510.0
โ
Multi-block coordination tests passed!
์ฑ๊ณต ๊ธฐ์ค:
- 4๊ฐ ๋ธ๋ก ๋ชจ๋ 0์ด ์๋ ๊ฒฐ๊ณผ๋ฅผ ์์ฑํฉ๋๋ค
- ๊ฒฐ๊ณผ๊ฐ ์ค์ผ์ผ๋ง ํจํด์ ๋ณด์ฌ์ค๋๋ค: Block 1 > Block 0, Block 2 > Block 1 ๋ฑ
- ๊ฒฝ์ ์ํ๋ ์กฐ์ ์คํจ๊ฐ ์์ด์ผ ํฉ๋๋ค
์๋ฃจ์
fn cluster_coordination_basics[
in_layout: Layout, out_layout: Layout, tpb: Int
](
output: LayoutTensor[dtype, out_layout, MutAnyOrigin],
input: LayoutTensor[dtype, in_layout, ImmutAnyOrigin],
size: Int,
):
"""Real cluster coordination using SM90+ cluster APIs."""
global_i = Int(block_dim.x * block_idx.x + thread_idx.x)
local_i = thread_idx.x
# Check what's happening with cluster ranks
my_block_rank = Int(block_rank_in_cluster())
block_id = Int(block_idx.x)
shared_data = LayoutTensor[
dtype,
Layout.row_major(tpb),
MutAnyOrigin,
address_space = AddressSpace.SHARED,
].stack_allocation()
# FIX: Use block_idx.x for data distribution instead of cluster rank
# Each block should process different portions of the data
var data_scale = Float32(
block_id + 1
) # Use block_idx instead of cluster rank
# Phase 1: Each block processes its portion
if global_i < size:
shared_data[local_i] = input[global_i] * data_scale
else:
shared_data[local_i] = 0.0
barrier()
# Phase 2: Use cluster_arrive() for inter-block coordination
cluster_arrive() # Signal this block has completed processing
# Block-level aggregation (only thread 0)
if local_i == 0:
var block_sum: Float32 = 0.0
for i in range(tpb):
block_sum += shared_data[i][0]
# FIX: Store result at block_idx position (guaranteed unique per block)
output[block_id] = block_sum
# Wait for all blocks in cluster to complete
cluster_wait()
ํด๋ฌ์คํฐ ์กฐ์ ํ์ด๋ ์ ์คํ๊ฒ ์ค๊ณ๋ 2๋จ๊ณ ์ ๊ทผ ๋ฐฉ์์ ํตํด ๊ธฐ๋ณธ์ ์ธ ๋ฉํฐ ๋ธ๋ก ๋๊ธฐํ ํจํด์ ๋ณด์ฌ์ค๋๋ค:
1๋จ๊ณ: ๋ ๋ฆฝ์ ๋ธ๋ก ์ฒ๋ฆฌ
์ค๋ ๋ ๋ฐ ๋ธ๋ก ์๋ณ:
global_i = block_dim.x * block_idx.x + thread_idx.x # Global thread index
local_i = thread_idx.x # Local thread index within block
my_block_rank = Int(block_rank_in_cluster()) # Cluster rank (0-3)
block_id = Int(block_idx.x) # Block index for reliable addressing
๊ณต์ ๋ฉ๋ชจ๋ฆฌ ํ ๋น ๋ฐ ๋ฐ์ดํฐ ์ฒ๋ฆฌ:
- ๊ฐ ๋ธ๋ก์ด ์์ฒด ๊ณต์ ๋ฉ๋ชจ๋ฆฌ ์์
๊ณต๊ฐ์ ํ ๋นํฉ๋๋ค:
LayoutTensor[dtype, Layout.row_major(tpb), MutAnyOrigin, address_space = AddressSpace.SHARED].stack_allocation() - ์ค์ผ์ผ๋ง ์ ๋ต:
data_scale = Float32(block_id + 1)๋ก ๊ฐ ๋ธ๋ก์ด ๋ค๋ฅด๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋๋ก ํฉ๋๋ค- Block 0: 1.0๋ฐฐ, Block 1: 2.0๋ฐฐ, Block 2: 3.0๋ฐฐ, Block 3: 4.0๋ฐฐ
- ๊ฒฝ๊ณ ๊ฒ์ฌ:
if global_i < size:๋ก ๋ฒ์ ๋ฐ ๋ฉ๋ชจ๋ฆฌ ์ ๊ทผ์ ๋ฐฉ์งํฉ๋๋ค - ๋ฐ์ดํฐ ์ฒ๋ฆฌ:
shared_data[local_i] = input[global_i] * data_scale๋ก ๋ธ๋ก๋ณ ์ ๋ ฅ ๋ฐ์ดํฐ๋ฅผ ์ค์ผ์ผ๋งํฉ๋๋ค
๋ธ๋ก ๋ด๋ถ ๋๊ธฐํ:
barrier()๋ ๊ฐ ๋ธ๋ก ๋ด ๋ชจ๋ ์ค๋ ๋๊ฐ ๋ฐ์ดํฐ ๋ก๋ฉ์ ์๋ฃํ ํ์์ผ ๋ค์ ๋จ๊ณ๋ก ์งํํ๋๋ก ๋ณด์ฅํฉ๋๋ค- ๋ฐ์ดํฐ ๋ก๋ฉ๊ณผ ์ดํ์ ํด๋ฌ์คํฐ ์กฐ์ ์ฌ์ด์ ๊ฒฝ์ ์ํ๋ฅผ ๋ฐฉ์งํฉ๋๋ค
2๋จ๊ณ: ํด๋ฌ์คํฐ ์กฐ์
๋ธ๋ก ๊ฐ ์ ํธ:
cluster_arrive()๋ ์ด ๋ธ๋ก์ด ๋ก์ปฌ ์ฒ๋ฆฌ ๋จ๊ณ๋ฅผ ์๋ฃํ์์ ์๋ฆฝ๋๋ค- ํด๋ฌ์คํฐ ํ๋์จ์ด์ ์๋ฃ๋ฅผ ๋ฑ๋กํ๋ ๋ ผ๋ธ๋กํน ์ฐ์ฐ์ ๋๋ค
๋ก์ปฌ ์ง๊ณ (์ค๋ ๋ 0๋ง):
if local_i == 0:
var block_sum: Float32 = 0.0
for i in range(tpb):
block_sum += shared_data[i][0] # Sum all elements in shared memory
output[block_id] = block_sum # Store result at unique block position
- ๊ฒฝ์ ์ํ๋ฅผ ํผํ๊ธฐ ์ํด ์ค๋ ๋ 0๋ง ํฉ์ฐ์ ์ํํฉ๋๋ค
output[block_id]์ ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅํ์ฌ ๊ฐ ๋ธ๋ก์ด ๊ณ ์ ํ ์์น์ ๊ธฐ๋กํ๋๋ก ํฉ๋๋ค
์ต์ข ๋๊ธฐํ:
cluster_wait()๋ ํด๋ฌ์คํฐ ๋ด ๋ชจ๋ ๋ธ๋ก์ด ์์ ์ ์๋ฃํ ๋๊น์ง ๋๊ธฐํฉ๋๋ค- ์ด๋ฅผ ํตํด ์ ์ฒด ํด๋ฌ์คํฐ์ ๊ฑธ์ณ ๊ฒฐ์ ๋ก ์ ์๋ฃ ์์๋ฅผ ๋ณด์ฅํฉ๋๋ค
ํต์ฌ ๊ธฐ์ ์ธ์ฌ์ดํธ
์ my_block_rank ๋์ block_id๋ฅผ ์ฌ์ฉํ ๊น?
block_idx.x๋ ์์ ์ ์ธ ๊ทธ๋ฆฌ๋ ์คํ ์ธ๋ฑ์ฑ์ ์ ๊ณตํฉ๋๋ค (0, 1, 2, 3)block_rank_in_cluster()๋ ํด๋ฌ์คํฐ ์ค์ ์ ๋ฐ๋ผ ๋ค๋ฅด๊ฒ ๋์ํ ์ ์์ต๋๋คblock_id๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฐ ๋ธ๋ก์ด ๊ณ ์ ํ ๋ฐ์ดํฐ ์์ญ๊ณผ ์ถ๋ ฅ ์์น๋ฅผ ํ๋ณดํ ์ ์์ต๋๋ค
๋ฉ๋ชจ๋ฆฌ ์ ๊ทผ ํจํด:
- ์ ์ญ ๋ฉ๋ชจ๋ฆฌ: ๊ฐ ์ค๋ ๋๊ฐ
input[global_i]๋ฅผ ์ ํํ ํ ๋ฒ ์ฝ์ต๋๋ค - ๊ณต์ ๋ฉ๋ชจ๋ฆฌ: ๋ธ๋ก ๋ด๋ถ ํต์ ๊ณผ ์ง๊ณ์ ์ฌ์ฉ๋ฉ๋๋ค
- ์ถ๋ ฅ ๋ฉ๋ชจ๋ฆฌ: ๊ฐ ๋ธ๋ก์ด
output[block_id]์ ์ ํํ ํ ๋ฒ ๊ธฐ๋กํฉ๋๋ค
๋๊ธฐํ ๊ณ์ธต ๊ตฌ์กฐ:
barrier(): ๊ฐ ๋ธ๋ก ๋ด ์ค๋ ๋๋ฅผ ๋๊ธฐํํฉ๋๋ค (๋ธ๋ก ๋ด๋ถ)cluster_arrive(): ๋ค๋ฅธ ๋ธ๋ก์ ์๋ฃ๋ฅผ ์๋ฆฝ๋๋ค (๋ธ๋ก ๊ฐ, ๋ ผ๋ธ๋กํน)cluster_wait(): ๋ชจ๋ ๋ธ๋ก์ด ์๋ฃ๋ ๋๊น์ง ๋๊ธฐํฉ๋๋ค (๋ธ๋ก ๊ฐ, ๋ธ๋กํน)
์ฑ๋ฅ ํน์ฑ:
- ์ฐ์ฐ ๋ณต์ก๋: ๋ธ๋ก๋น ๋ก์ปฌ ํฉ์ฐ์ O(TPB), ํด๋ฌ์คํฐ ์กฐ์ ์ O(1)
- ๋ฉ๋ชจ๋ฆฌ ๋์ญํญ: ๊ฐ ์ ๋ ฅ ์์๋ฅผ ํ ๋ฒ๋ง ์ฝ์ผ๋ฉฐ, ๋ธ๋ก ๊ฐ ํต์ ์ ์ต์ํ
- ํ์ฅ์ฑ: ํจํด์ด ๋ ํฐ ํด๋ฌ์คํฐ ํฌ๊ธฐ์๋ ์ต์ํ์ ์ค๋ฒํค๋๋ก ํ์ฅ ๊ฐ๋ฅ
ํจํด ์ดํดํ๊ธฐ
ํด๋ฌ์คํฐ ์กฐ์ ์ ํต์ฌ ํจํด์ ๋จ์ํ์ง๋ง ๊ฐ๋ ฅํ ๊ตฌ์กฐ๋ฅผ ๋ฐ๋ฆ ๋๋ค:
- 1๋จ๊ณ: ๊ฐ ๋ธ๋ก์ด ํ ๋น๋ ๋ฐ์ดํฐ ์์ญ์ ๋ ๋ฆฝ์ ์ผ๋ก ์ฒ๋ฆฌํฉ๋๋ค
- ์ ํธ:
cluster_arrive()๋ก ์ฒ๋ฆฌ ์๋ฃ๋ฅผ ์๋ฆฝ๋๋ค - 2๋จ๊ณ: ๋ค๋ฅธ ๋ธ๋ก์ ๊ฒฐ๊ณผ์ ์์กดํ๋ ์ฐ์ฐ์ ์์ ํ๊ฒ ์ํํ ์ ์์ต๋๋ค
- ๋๊ธฐํ:
cluster_wait()๋ก ๋ชจ๋ ๋ธ๋ก์ด ์๋ฃ๋ ํ ๋ค์์ผ๋ก ์งํํฉ๋๋ค
๋ค์ ๋จ๊ณ: ๋ ๊ณ ๊ธ ์กฐ์ ์ ๋ฐฐ์ธ ์ค๋น๊ฐ ๋์
จ๋์? ํด๋ฌ์คํฐ ์ ์ฒด ์งํฉ ์ฐ์ฐ ์ผ๋ก ์ด๋ํ์ฌ Puzzle 27์ block.sum() ํจํด์ ํด๋ฌ์คํฐ ๊ท๋ชจ๋ก ํ์ฅํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์. Puzzle 24์ ์ํ ๋ ๋ฒจ ๋ฆฌ๋์
์ ๊ธฐ๋ฐ์ผ๋ก ํฉ๋๋ค!