교수님께서 한가지 제안을 해주셨다.
과연 묶어서 수행할 경우와 LBL (layer by layer)로 수행할 경우에는 수행시간에 차이가 날까?
내부적으로 어떤 병렬 방식이 있어서, 묶어서 수행하는 경우가 더 짧지 않을까?
에 대한 의문점을 제시해 주셨고, 이를 확인하고자 실험을 구성하였다.
먼저 설명한 '묶어서 수행할 경우와
LBL로 수행할 경우' 가 너무 모호한 설명이기 때문에, 그림과 함께 설명해 본다.
LBL로 수행할 경우에 위에 적어둔 순서대로 layer를 통과하게 된다. 여기서 궁금한것은, LBL수행이 아닌 그저 model을 call (또는 predict, inference)하는 경우와 시간이 다르게 측정되는지 확인하고자 하였다.
먼저 실험을 진행한 컴퓨터의 스펙이다.
CPU: i5-3570
RAM: 8GB
GPU: GTX970
Tensorflow version: 2.3.0
CUDA version: 10.1
Dataset: MNIST dataset
아래는 모델을 구성한 코드이다.
input = layers.Input(shape=(28,28,1))
conv1 = layers.Conv2D(32,(3,3),activation='relu')(input)
pool1 = layers.MaxPool2D((2,2))(conv1)
conv21 = layers.Conv2D(64, (3, 3), activation='relu')(pool1)
pool21 = layers.MaxPooling2D((2, 2))(conv21)
conv22 = layers.Conv2D(64, (5, 5), padding='valid', activation='relu')(pool1)
pool22 = layers.MaxPooling2D((2, 2))(conv22)
conv31 = layers.Conv2D(64, (3, 3), activation='relu')(pool21)
pool31 = layers.MaxPooling2D((2, 2))(conv31)
conv32 = layers.Conv2D(64, (3, 3), padding='valid', activation='relu')(pool22)
pool32 = layers.MaxPooling2D((2, 2))(conv32)
flat1 = layers.Flatten()(pool31)
flat2 = layers.Flatten()(pool32)
dense1 = layers.Dense(32, activation='relu')(flat1)
dense2 = layers.Dense(32, activation='relu')(flat2)
merge = layers.concatenate([dense1, dense2])
output = layers.Dense(10,activation='softmax')(merge)
model = models.Model(inputs=input, outputs=output)
conv22에서 kernel 크기와 padding='valid'로 수행한 이유는 conv21과 conv22가 동일한 경우에 측정한 수행시간은
conv21의 시간은 측정되었지만 conv22의 시간은 0 ns로 측정되었다.
해당 이유에 대해서는 본인은 다음과 같이 생각하고 있다.
input과 output이 동일하고, 동일한 커널을 사용하기 때문에 내부적으로 동일한 연산이 진행됨을 알아내서, 추가적인 연산을 진행하지 않았다고 생각한다.
따라서 conv22의 경우에는 kernel크기도 다르게 설정하고 padding도 추가하여 확실히 다른 연산이 진행됨을 보여주고자 하였다.
아래는 LBL로 수행하여 시간을 측정하는 코드이다.
dummyInput = test_images[0]
dummyInput = np.expand_dims(dummyInput,0)
predecessorInfo = {}
OutputSaver={}
model.summary()
layerExecutionTime={}
for i in range(len(model.layers)):
layer_x = model.get_layer(index=i)
int_node = layer_x._inbound_nodes[0]
predecessorInfo[layer_x.name]=[]
try:
for predecessor_layers in int_node.inbound_layers:
predecessorInfo[layer_x.name].append(predecessor_layers.name)
except:
predecessorInfo[layer_x.name].append(int_node.inbound_layers.name)
result = np.expand_dims(test_images[0],0)
result = result.astype(np.float32)
for layer in model.layers:
if predecessorInfo[layer.name]:
if len(predecessorInfo[layer.name]) == 1:
result = OutputSaver[predecessorInfo[layer.name][0]]
startTime = time.process_time_ns()
result = layer.call(result)
endTime = time.process_time_ns()
OutputSaver[layer.name] = result
else:
result=[]
for predecessor in predecessorInfo[layer.name]:
result.append(OutputSaver[predecessor])
startTime = time.process_time_ns()
result = layer.call(result)
endTime = time.process_time_ns()
OutputSaver[layer.name] = result
layerExecutionTime[layer.name] = endTime-startTime
else:
startTime = time.process_time_ns()
result = layer.call(result)
endTime = time.process_time_ns()
OutputSaver[layer.name] = result
layerExecutionTime[layer.name] = endTime-startTime
print(layerExecutionTime)
for idx, rlt in enumerate(result):
print(test_labels[idx])
print(tf.argmax(rlt,0))
predecessorInfo는 각 레이어의 조상이 누구인지 저장하는 dictionary이다.
2개 이상의 레이어를 합치는 경우에는 조상이 2명인 경우가 있기 때문에, 조상의 정보는 필요하다.
OutputSaver는 key: layer.name, value: layer의 output 으로 이뤄진 dictionary이다.
조상이 2개 이상인 경우에는 조상들의 output을 합쳐야 하기 때문에, 모든 layer의 Output을 저장하며, 값이 저장되어있다면 언제든지 불러올 수 있다.
항상 ML inference에서 느꼈지만, 첫번째 추론은 시간이 오래걸리며, 그 이후의 추론들은 모두 비슷한 시간이 걸렸다.
따라서 이번 실험에서는 두가지에 대해 측정하고자 한다.
1. 첫번째 추론에서 수행 시간 측정
2. 첫번째 추론은 dummy data를 이용하여 측정하지 않고, 두번째 추론에서 수행 시간 측정
1. 첫번째 추론에서 수행시간 측정 (1장)
다음 표는 LBL과 predict한 경우의 시간 차이를 보여준다. LBL의 경우 그대로 측정할 수 있지만 묶어서 실행하는 경우에는 총 3개의 모델로 나눌 수 있는데, 혹시라도 첫번째 추론 시간에 영향을 줄까 걱정되어 model 그대로 측정하기로 하였다.
첫번째 추론에서 수행시간 측정은 총 10회를 진행하였고, 다음 표는 평균 시간을 나타낸다.
LBL (SUM) | model(Input) | model.predict(Input) | |
process time (nano second) | 1,079,687,500 | 1,079,687,500 | 1,234,375,000 |
process time (second) | 1.0796875 | 1.0796875 | 1.234375 |
첫번째 측정에서는 LBL의 sum 결과가 predict보다 적었다.
그러나 model을 call하는 경우에는 우연스럽게도 LBL과 동일한 결과를 가져왔다.
1. 두번째 추론부터 수행시간 측정 (100장)
첫번째 추론시간을 포함하지 않고,총 100회를 진행하여 수행 시간을 측정하여 평균내었다.
코드는 다음과 같이 작성하여 100회 수행하였다.
exectime=0
for dummyInput in dummyInputs:
startTime=time.process_time_ns()
rlt = model.predict(dummyInput)
endTime = time.process_time_ns()
exectime+=(endTime-startTime)
print(f'model.predict(): {exectime/100}')
LBL의 경우에도 유사한 방식으로 다음과 같이 측정하였다.
ans=[]
for dummyInput in dummyInputs:
result = dummyInput
for layer in model.layers:
# print(layer.name)
if predecessorInfo[layer.name]:
if len(predecessorInfo[layer.name]) == 1:
result = OutputSaver[predecessorInfo[layer.name][0]]
startTime = time.process_time_ns()
result = layer.call(result)
endTime = time.process_time_ns()
OutputSaver[layer.name] = result
else:
result=[]
for predecessor in predecessorInfo[layer.name]:
result.append(OutputSaver[predecessor])
startTime = time.process_time_ns()
result = layer.call(result)
endTime = time.process_time_ns()
OutputSaver[layer.name] = result
layerExecutionTime[layer.name] = endTime-startTime
else:
startTime = time.process_time_ns()
result = layer.call(result)
endTime = time.process_time_ns()
OutputSaver[layer.name] = result
layerExecutionTime[layer.name] = endTime-startTime
ans.append(result)
코드에서 보이지는 않지만, 두 경우 모두 첫번째 predict 또는 LBL을 수행하여, 두번째 시간부터 측정하였다.
predict의 경우에서 한 번에 100장을 수행할 때까지를 포함하여 측정해보았다.
LBL (SUM) | model(Input) | model.predict(Input) | model.predict(Input[:100]) | |
process time (nano second) | 156,250 | 3,750,000 | 35,468,750 | 218,750,000 |
process time (second) | 0.00015625 | 0.00375 | 0.03546875 | 0.21875 |
사실 LBL의 실험 결과에서는 대부분 0초가 나왔었지만, 여러 시행중 그나마 숫자가 큰 경우를 가져왔다.
100장을 한번에 predict 하는 경우에도 첫번째 추론을 제외하고 측정하였지만 예상과는 너무 다른 결과가 나왔다.
그나마 model call하는 경우는 그나마 작았지만, LBL에 비하면 너무 컸다 (24배)
1. 모델이 너무 간단해서 그런 것인가?
- 아직 Yolo 에서 LBL 수행이 불안하지만, Yolo에서 수행했을 때는 LBL이 더 느렸었다.
2. 묶어서 수행하는것이 더 느린가?
- 이 실험만 두고 단정지을수는 없다고 생각한다.
궁금증을 해결하기 위해서 실험을 하였지만, 의문점이 좀더 늘어나게 된것 같다...
'끄적끄적' 카테고리의 다른 글
TFLite visualizing (0) | 2023.04.06 |
---|---|
Yolo v4 Set up (0) | 2023.02.23 |
TensorFlow관련 주저리... (0) | 2023.02.06 |
Non-Sequential Model 묶기 - Part 2 (2) | 2023.02.03 |
Non-Sequential Model 묶기 (0) | 2023.02.02 |