Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
Oneflow-Inc
oneflow
提交
83fc29e6
O
oneflow
项目概览
Oneflow-Inc
/
oneflow
上一次同步 2 年多
通知
13
Star
2733
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
O
oneflow
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
前往新版Gitcode,体验更适合开发者的 AI 搜索 >>
提交
83fc29e6
编写于
11月 28, 2020
作者:
L
lixinqi
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
merge master
Former-commit-id: a4ce93e0dbc838e912b4b416e321d77449d3a6a5
上级
c46b52b5
变更
8
隐藏空白更改
内联
并排
Showing
8 changed file
with
166 addition
and
76 deletion
+166
-76
oneflow/core/job/job_build_and_infer_ctx.cpp
oneflow/core/job/job_build_and_infer_ctx.cpp
+1
-1
oneflow/core/job/ssp_config_def.cpp
oneflow/core/job/ssp_config_def.cpp
+2
-9
oneflow/core/job/stage_config_def.cpp
oneflow/core/job/stage_config_def.cpp
+35
-0
oneflow/core/job_rewriter/add_ssp_variable_proxy.cpp
oneflow/core/job_rewriter/add_ssp_variable_proxy.cpp
+3
-3
oneflow/core/job_rewriter/stage_partition_pass.cpp
oneflow/core/job_rewriter/stage_partition_pass.cpp
+43
-43
oneflow/python/framework/attr_util.py
oneflow/python/framework/attr_util.py
+3
-0
oneflow/python/framework/function_util.py
oneflow/python/framework/function_util.py
+5
-20
oneflow/python/test/ops/test_stage_partition.py
oneflow/python/test/ops/test_stage_partition.py
+74
-0
未找到文件。
oneflow/core/job/job_build_and_infer_ctx.cpp
浏览文件 @
83fc29e6
...
...
@@ -958,7 +958,7 @@ Maybe<void> LazyJobBuildAndInferCtx::Complete() {
JUST
(
DoPass
(
"DynamicLossScaleSchedulePass"
));
JUST
(
DoPass
(
"AutoTrainStep"
));
JUST
(
DoPass
(
"AutoLearningRate"
));
JUST
(
DoPass
(
"S
sp
Partition"
));
JUST
(
DoPass
(
"S
tage
Partition"
));
JUST
(
DoPass
(
"GenerateBackwardAndOptimizerOpConfs"
));
JUST
(
DoPass
(
"AddSspVariableProxy"
));
JUST
(
DoPass
(
"CudnnFusedNormalizationAddReluPass"
));
...
...
oneflow/core/job/ssp_config_def.cpp
浏览文件 @
83fc29e6
...
...
@@ -19,15 +19,8 @@ namespace oneflow {
namespace
{
REGISTER_FUNCTION_CONFIG_DEF
()
.
Bool
(
"enable_ssp"
,
false
,
"enable ssp"
)
.
String
(
"ssp_partition_strategy"
,
"naive_sequantial"
,
"ssp partition strategy, Avaiable strategies: naive_sequantial | disable"
)
.
ListInt64
(
"ssp_partition_scope_ids"
,
{},
"type: list[int64]. ssp partition scope symbol ids"
);
REGISTER_SCOPE_CONFIG_DEF
()
.
Int64
(
"ssp_num_stages"
,
-
1
,
"total number of ssp stages"
)
.
Int64
(
"ssp_stage_id"
,
-
1
,
"current ssp stage id "
);
REGISTER_FUNCTION_CONFIG_DEF
().
Bool
(
"enable_ssp_variable_proxy"
,
false
,
"enable ssp variable proxy"
);
}
// namespace
...
...
oneflow/core/job/stage_config_def.cpp
0 → 100644
浏览文件 @
83fc29e6
/*
Copyright 2020 The OneFlow Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "oneflow/core/framework/config_def.h"
namespace
oneflow
{
namespace
{
REGISTER_FUNCTION_CONFIG_DEF
()
.
Bool
(
"enable_stage_partition"
,
false
,
"enable stage partition"
)
.
String
(
"stage_partition_strategy"
,
"naive_sequantial"
,
"stage partition strategy, Avaiable strategies: naive_sequantial | disable"
)
.
ListInt64
(
"stage_partition_scope_ids"
,
{},
"type: list[int64]. stage partition scope symbol ids"
);
REGISTER_SCOPE_CONFIG_DEF
()
.
Int64
(
"num_stages"
,
-
1
,
"total number of stages"
)
.
Int64
(
"stage_id"
,
-
1
,
"current stage id "
);
}
// namespace
}
// namespace oneflow
oneflow/core/job_rewriter/add_ssp_variable_proxy.cpp
浏览文件 @
83fc29e6
...
...
@@ -40,7 +40,7 @@ class AddSspVariableProxyPass final : public JobPass {
}
bool
IsEnabled
(
const
JobPassCtx
&
ctx
)
const
{
return
ctx
.
job_desc
().
IsTrain
()
&&
ctx
.
job_desc
().
Bool
(
"enable_ssp"
);
return
ctx
.
job_desc
().
IsTrain
()
&&
ctx
.
job_desc
().
Bool
(
"enable_ssp
_variable_proxy
"
);
}
Maybe
<
void
>
Apply
(
const
OpGraph
&
op_graph
,
JobBuilder
*
job_builder
)
const
{
...
...
@@ -125,8 +125,8 @@ class AddSspVariableProxyPass final : public JobPass {
const
Scope
&
scope
=
JUST
(
Global
<
vm
::
SymbolStorage
<
Scope
>>::
Get
()
->
MaybeGet
(
scope_symbol_id
));
int64_t
buffer_size
=
0
;
{
int64_t
num_stages
=
scope
.
Int64
(
"
ssp_
num_stages"
);
int64_t
stage_id
=
scope
.
Int64
(
"s
sp_s
tage_id"
);
int64_t
num_stages
=
scope
.
Int64
(
"num_stages"
);
int64_t
stage_id
=
scope
.
Int64
(
"stage_id"
);
CHECK_GT
(
num_stages
,
0
);
CHECK_GE
(
stage_id
,
0
);
CHECK_LT
(
stage_id
,
num_stages
);
...
...
oneflow/core/job_rewriter/s
sp
_partition_pass.cpp
→
oneflow/core/job_rewriter/s
tage
_partition_pass.cpp
浏览文件 @
83fc29e6
...
...
@@ -26,55 +26,55 @@ namespace oneflow {
namespace
{
class
S
sp
PartitionStragety
{
class
S
tage
PartitionStragety
{
public:
S
sp
PartitionStragety
()
=
default
;
~
S
sp
PartitionStragety
()
=
default
;
S
tage
PartitionStragety
()
=
default
;
~
S
tage
PartitionStragety
()
=
default
;
virtual
Maybe
<
void
>
Apply
(
Job
*
job
,
JobPassCtx
*
ctx
)
const
=
0
;
};
class
S
sp
PartitionPass
final
:
public
JobPass
{
class
S
tage
PartitionPass
final
:
public
JobPass
{
public:
S
sp
PartitionPass
()
=
default
;
~
S
sp
PartitionPass
()
=
default
;
S
tage
PartitionPass
()
=
default
;
~
S
tage
PartitionPass
()
=
default
;
Maybe
<
void
>
Apply
(
Job
*
job
,
JobPassCtx
*
ctx
)
const
override
{
if
(
!
IsEnabled
(
*
ctx
))
{
return
Maybe
<
void
>::
Ok
();
}
const
std
::
string
&
partition_strategy
=
ctx
->
job_desc
().
String
(
"s
sp
_partition_strategy"
);
std
::
unique_ptr
<
const
S
sp
PartitionStragety
>
strategy
;
strategy
.
reset
(
NewObj
<
std
::
string
,
S
sp
PartitionStragety
>
(
partition_strategy
));
const
std
::
string
&
partition_strategy
=
ctx
->
job_desc
().
String
(
"s
tage
_partition_strategy"
);
std
::
unique_ptr
<
const
S
tage
PartitionStragety
>
strategy
;
strategy
.
reset
(
NewObj
<
std
::
string
,
S
tage
PartitionStragety
>
(
partition_strategy
));
return
strategy
->
Apply
(
job
,
ctx
);
}
bool
IsEnabled
(
const
JobPassCtx
&
ctx
)
const
{
return
ctx
.
job_desc
().
IsTrain
()
&&
ctx
.
job_desc
().
Bool
(
"enable_s
sp
"
);
return
ctx
.
job_desc
().
IsTrain
()
&&
ctx
.
job_desc
().
Bool
(
"enable_s
tage_partition
"
);
}
};
REGISTER_JOB_PASS
(
"S
spPartition"
,
Ssp
PartitionPass
);
REGISTER_JOB_PASS
(
"S
tagePartition"
,
Stage
PartitionPass
);
#define REGISTER_SSP_PARTITION_STRATEGY(strategy_name, strategy_type) \
REGISTER_CLASS_CREATOR(std::string, strategy_name, S
sp
PartitionStragety, \
#define REGISTER_SSP_PARTITION_STRATEGY(strategy_name, strategy_type)
\
REGISTER_CLASS_CREATOR(std::string, strategy_name, S
tage
PartitionStragety, \
([] { return new strategy_type(); }));
class
DisableS
spPartitionStrategy
:
public
Ssp
PartitionStragety
{
class
DisableS
tagePartitionStrategy
:
public
Stage
PartitionStragety
{
public:
DisableS
sp
PartitionStrategy
()
=
default
;
~
DisableS
sp
PartitionStrategy
()
=
default
;
DisableS
tage
PartitionStrategy
()
=
default
;
~
DisableS
tage
PartitionStrategy
()
=
default
;
Maybe
<
void
>
Apply
(
Job
*
job
,
JobPassCtx
*
)
const
override
{
return
Maybe
<
void
>::
Ok
();
}
};
REGISTER_SSP_PARTITION_STRATEGY
(
"disable"
,
DisableS
sp
PartitionStrategy
);
REGISTER_SSP_PARTITION_STRATEGY
(
"disable"
,
DisableS
tage
PartitionStrategy
);
class
NaiveSequantialS
spPartitionStrategy
:
public
Ssp
PartitionStragety
{
class
NaiveSequantialS
tagePartitionStrategy
:
public
Stage
PartitionStragety
{
public:
NaiveSequantialS
sp
PartitionStrategy
()
=
default
;
~
NaiveSequantialS
sp
PartitionStrategy
()
=
default
;
NaiveSequantialS
tage
PartitionStrategy
()
=
default
;
~
NaiveSequantialS
tage
PartitionStrategy
()
=
default
;
Maybe
<
void
>
Apply
(
Job
*
job
,
JobPassCtx
*
ctx
)
const
override
{
const
OpGraph
op_graph
(
*
job
);
JobBuilder
job_builder
(
job
);
JUST
(
ForEachS
sp
Scope4TrainableFwOp
(
JUST
(
ForEachS
tage
Scope4TrainableFwOp
(
op_graph
,
ctx
->
job_desc
(),
[
&
](
const
OpNode
*
op_node
,
const
Scope
&
scope
,
int64_t
scope_symbol_id
)
->
Maybe
<
void
>
{
// Sets scope_symbol_id
...
...
@@ -93,28 +93,28 @@ class NaiveSequantialSspPartitionStrategy : public SspPartitionStragety {
}
private:
Maybe
<
void
>
ForEachS
sp
Scope4TrainableFwOp
(
Maybe
<
void
>
ForEachS
tage
Scope4TrainableFwOp
(
const
OpGraph
&
op_graph
,
const
JobDesc
&
job_desc
,
const
std
::
function
<
Maybe
<
void
>
(
const
OpNode
*
,
const
Scope
&
,
int64_t
scope_symbol_id
)
>&
Handler
)
const
{
// Sequantialize trainable forward ops
std
::
list
<
std
::
unique_ptr
<
std
::
vector
<
OpNode
*>>>
sequantial_trainable_fw_ops
;
JUST
(
GetSequantialTrainableFwOps
(
op_graph
,
&
sequantial_trainable_fw_ops
));
// Gets s
sp
partition config
std
::
vector
<
int64_t
>
s
sp
_partition_scope_ids
;
JUST
(
GetS
spPartitionScopeIds
(
job_desc
,
&
ssp
_partition_scope_ids
));
// Gets s
tage
partition config
std
::
vector
<
int64_t
>
s
tage
_partition_scope_ids
;
JUST
(
GetS
tagePartitionScopeIds
(
job_desc
,
&
stage
_partition_scope_ids
));
// Partition to stages
std
::
function
<
Maybe
<
int64_t
>
(
int64_t
)
>
Stage4Depth
;
int64_t
num_stages
=
s
sp
_partition_scope_ids
.
size
();
JUST
(
GetS
sp
Depth2Stage
(
sequantial_trainable_fw_ops
,
num_stages
,
&
Stage4Depth
));
std
::
function
<
Maybe
<
const
Scope
&>
(
int64_t
,
int64_t
*
)
>
S
sp
Scope4Stage
;
int64_t
num_stages
=
s
tage
_partition_scope_ids
.
size
();
JUST
(
GetS
tage
Depth2Stage
(
sequantial_trainable_fw_ops
,
num_stages
,
&
Stage4Depth
));
std
::
function
<
Maybe
<
const
Scope
&>
(
int64_t
,
int64_t
*
)
>
S
tage
Scope4Stage
;
// Provides scope for each stage
JUST
(
MakeGetterS
spScope4Stage
(
ssp_partition_scope_ids
,
&
Ssp
Scope4Stage
));
JUST
(
MakeGetterS
tageScope4Stage
(
stage_partition_scope_ids
,
&
Stage
Scope4Stage
));
int64_t
depth
=
0
;
for
(
const
auto
&
fused_vec
:
sequantial_trainable_fw_ops
)
{
int64_t
stage
=
JUST
(
Stage4Depth
(
depth
));
int64_t
scope_symbol_id
=
0
;
const
auto
&
scope
=
JUST
(
S
sp
Scope4Stage
(
stage
,
&
scope_symbol_id
));
const
auto
&
scope
=
JUST
(
S
tage
Scope4Stage
(
stage
,
&
scope_symbol_id
));
for
(
OpNode
*
op_node
:
*
fused_vec
)
{
JUST
(
Handler
(
op_node
,
scope
,
scope_symbol_id
));
}
++
depth
;
}
...
...
@@ -156,7 +156,7 @@ class NaiveSequantialSspPartitionStrategy : public SspPartitionStragety {
return
Maybe
<
void
>::
Ok
();
}
Maybe
<
void
>
GetS
sp
Depth2Stage
(
Maybe
<
void
>
GetS
tage
Depth2Stage
(
const
std
::
list
<
std
::
unique_ptr
<
std
::
vector
<
OpNode
*>>>&
sequantial_trainable_fw_ops
,
int64_t
num_stages
,
std
::
function
<
Maybe
<
int64_t
>
(
int64_t
)
>*
Stage4Depth
)
const
{
int64_t
num_ops
=
0
;
...
...
@@ -303,29 +303,29 @@ class NaiveSequantialSspPartitionStrategy : public SspPartitionStragety {
return
Maybe
<
void
>::
Ok
();
}
Maybe
<
void
>
MakeGetterS
sp
Scope4Stage
(
const
std
::
vector
<
int64_t
>&
s
sp
_partition_scope_ids
,
std
::
function
<
Maybe
<
const
Scope
&>
(
int64_t
stage
,
int64_t
*
scope_symbol_id
)
>*
S
sp
Scope4Stage
)
Maybe
<
void
>
MakeGetterS
tage
Scope4Stage
(
const
std
::
vector
<
int64_t
>&
s
tage
_partition_scope_ids
,
std
::
function
<
Maybe
<
const
Scope
&>
(
int64_t
stage
,
int64_t
*
scope_symbol_id
)
>*
S
tage
Scope4Stage
)
const
{
*
S
spScope4Stage
=
[
ssp_partition_scope_ids
](
int64_t
stage
,
int64_t
*
scope_symbol_id
)
->
Maybe
<
const
Scope
&>
{
*
S
tageScope4Stage
=
[
stage_partition_scope_ids
](
int64_t
stage
,
int64_t
*
scope_symbol_id
)
->
Maybe
<
const
Scope
&>
{
CHECK_GE_OR_RETURN
(
stage
,
0
);
CHECK_LT_OR_RETURN
(
stage
,
s
sp
_partition_scope_ids
.
size
());
*
scope_symbol_id
=
s
sp
_partition_scope_ids
.
at
(
stage
);
CHECK_LT_OR_RETURN
(
stage
,
s
tage
_partition_scope_ids
.
size
());
*
scope_symbol_id
=
s
tage
_partition_scope_ids
.
at
(
stage
);
return
Global
<
vm
::
SymbolStorage
<
Scope
>>::
Get
()
->
Get
(
*
scope_symbol_id
);
};
return
Maybe
<
void
>::
Ok
();
}
Maybe
<
void
>
GetS
sp
PartitionScopeIds
(
const
JobDesc
&
job_desc
,
std
::
vector
<
int64_t
>*
ssp
_partition_scope_ids
)
const
{
const
auto
&
scope_ids
=
job_desc
.
ListInt64
(
"s
sp
_partition_scope_ids"
);
Maybe
<
void
>
GetS
tage
PartitionScopeIds
(
const
JobDesc
&
job_desc
,
std
::
vector
<
int64_t
>*
stage
_partition_scope_ids
)
const
{
const
auto
&
scope_ids
=
job_desc
.
ListInt64
(
"s
tage
_partition_scope_ids"
);
CHECK_GT_OR_RETURN
(
scope_ids
.
size
(),
0
);
s
sp
_partition_scope_ids
->
assign
(
scope_ids
.
begin
(),
scope_ids
.
end
());
s
tage
_partition_scope_ids
->
assign
(
scope_ids
.
begin
(),
scope_ids
.
end
());
return
Maybe
<
void
>::
Ok
();
}
};
REGISTER_SSP_PARTITION_STRATEGY
(
"naive_sequantial"
,
NaiveSequantialS
sp
PartitionStrategy
);
REGISTER_SSP_PARTITION_STRATEGY
(
"naive_sequantial"
,
NaiveSequantialS
tage
PartitionStrategy
);
}
// namespace
...
...
oneflow/python/framework/attr_util.py
浏览文件 @
83fc29e6
...
...
@@ -30,6 +30,9 @@ def SetAttrValue(attr_value, py_value, default_attr_value):
elif
default_attr_value
.
HasField
(
"at_string"
):
assert
type
(
py_value
)
is
str
attr_value
.
at_string
=
py_value
elif
default_attr_value
.
HasField
(
"at_list_int64"
):
assert
type
(
py_value
)
is
list
attr_value
.
at_list_int64
.
val
[:]
=
py_value
else
:
raise
ValueError
(
"config with type %s is invalid. supported types: [bool, int, float, str]"
...
...
oneflow/python/framework/function_util.py
浏览文件 @
83fc29e6
...
...
@@ -33,6 +33,7 @@ import oneflow.python.framework.placement_context as placement_ctx
import
oneflow.python.framework.session_context
as
session_ctx
import
oneflow.python.framework.typing_util
as
oft_util
import
oneflow.python.lib.core.pb_util
as
pb_util
import
oneflow.python.framework.attr_util
as
attr_util
from
oneflow.python.framework.function_desc
import
FunctionDesc
from
oneflow.python.oneflow_export
import
oneflow_export
import
traceback
...
...
@@ -56,27 +57,11 @@ class FunctionConfig(object):
default_val
=
name2default
[
attr_name
]
def
FunctionConfigSetter
(
attr
_value
:
Optional
[
Union
[
bool
,
int
,
float
,
str
]]
=
None
py
_value
:
Optional
[
Union
[
bool
,
int
,
float
,
str
]]
=
None
)
->
None
:
if
default_val
.
HasField
(
"at_bool"
):
if
attr_value
is
None
:
attr_value
=
True
assert
type
(
attr_value
)
is
bool
flag_name2flag_value
[
attr_name
].
at_bool
=
attr_value
elif
default_val
.
HasField
(
"at_int64"
):
assert
type
(
attr_value
)
is
int
flag_name2flag_value
[
attr_name
].
at_int64
=
attr_value
elif
default_val
.
HasField
(
"at_double"
):
assert
type
(
attr_value
)
is
float
flag_name2flag_value
[
attr_name
].
at_double
=
attr_value
elif
default_val
.
HasField
(
"at_string"
):
assert
type
(
attr_value
)
is
str
flag_name2flag_value
[
attr_name
].
at_string
=
attr_value
else
:
raise
NotImplementedError
(
"config_flag `%s' with type %s is not supported"
%
(
attr_name
,
type
(
attr_value
))
)
attr_util
.
SetAttrValue
(
flag_name2flag_value
[
attr_name
],
py_value
,
default_val
)
return
FunctionConfigSetter
...
...
oneflow/python/test/ops/test_stage_partition.py
0 → 100644
浏览文件 @
83fc29e6
"""
Copyright 2020 The OneFlow Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import
oneflow
as
flow
import
numpy
as
np
import
oneflow.typing
as
tp
import
os
import
unittest
@
flow
.
unittest
.
skip_unless_1n1d
()
class
TestStagePartition
(
flow
.
unittest
.
TestCase
):
def
GetScopeSymbolIds
(
self
,
device_tag
,
device_name
,
num
):
scope_symbol_ids
=
[]
for
i
in
range
(
num
):
with
flow
.
scope
.
placement
(
device_tag
,
device_name
):
scope_symbol_ids
.
append
(
flow
.
current_scope
().
symbol_id
)
return
scope_symbol_ids
def
test_stage_partition
(
self
):
if
flow
.
eager_execution_enabled
():
return
device_name
=
"0:0"
flow
.
config
.
enable_debug_mode
(
True
)
flow
.
config
.
cpu_device_num
(
2
)
shape
=
(
10
,)
function_config
=
flow
.
FunctionConfig
()
function_config
.
enable_stage_partition
(
True
)
function_config
.
stage_partition_scope_ids
(
self
.
GetScopeSymbolIds
(
"gpu"
,
device_name
,
2
)
)
@
flow
.
global_function
(
type
=
"train"
,
function_config
=
function_config
)
def
Foo
()
->
tp
.
Numpy
:
x
=
flow
.
constant
(
0
,
dtype
=
flow
.
float
,
shape
=
shape
)
with
flow
.
scope
.
placement
(
"gpu"
,
device_name
):
for
i
in
range
(
10
):
w
=
flow
.
get_variable
(
"w_%s"
%
i
,
shape
=
shape
,
dtype
=
flow
.
float
,
initializer
=
flow
.
constant_initializer
(
0
),
)
x
=
w
+
x
loss
=
x
flow
.
optimizer
.
SGD
(
flow
.
optimizer
.
PiecewiseConstantScheduler
([],
[
-
10.0
]),
momentum
=
0
).
minimize
(
loss
)
return
loss
checkpoint
=
flow
.
train
.
CheckPoint
()
checkpoint
.
init
()
for
i
in
range
(
10
):
x
=
Foo
()
if
__name__
==
"__main__"
:
unittest
.
main
()
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录