Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
OpenDAS
nni
Commits
e9f137f0
You need to sign in or sign up before continuing.
Unverified
Commit
e9f137f0
authored
Feb 09, 2020
by
QuanluZhang
Committed by
GitHub
Feb 09, 2020
Browse files
merge from master (#2019)
parent
f7cf3ea5
Changes
107
Show whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
466 additions
and
31 deletions
+466
-31
src/nni_manager/rest_server/restValidationSchemas.ts
src/nni_manager/rest_server/restValidationSchemas.ts
+1
-0
src/nni_manager/training_service/pai/paiK8S/paiK8SConfig.ts
src/nni_manager/training_service/pai/paiK8S/paiK8SConfig.ts
+3
-1
src/nni_manager/training_service/pai/paiK8S/paiK8STrainingService.ts
...ager/training_service/pai/paiK8S/paiK8STrainingService.ts
+19
-2
src/nni_manager/yarn.lock
src/nni_manager/yarn.lock
+5
-0
src/sdk/pynni/nni/compression/torch/pruners.py
src/sdk/pynni/nni/compression/torch/pruners.py
+1
-1
src/sdk/pynni/nni/medianstop_assessor/test.py
src/sdk/pynni/nni/medianstop_assessor/test.py
+3
-3
src/sdk/pynni/nni/nas/pytorch/classic_nas/mutator.py
src/sdk/pynni/nni/nas/pytorch/classic_nas/mutator.py
+9
-0
src/sdk/pynni/nni/nas/pytorch/darts/mutator.py
src/sdk/pynni/nni/nas/pytorch/darts/mutator.py
+19
-14
src/sdk/pynni/nni/nas/pytorch/fixed.py
src/sdk/pynni/nni/nas/pytorch/fixed.py
+6
-6
src/sdk/pynni/nni/nas/pytorch/utils.py
src/sdk/pynni/nni/nas/pytorch/utils.py
+8
-0
src/sdk/pynni/tests/models/pytorch_models/__init__.py
src/sdk/pynni/tests/models/pytorch_models/__init__.py
+6
-0
src/sdk/pynni/tests/models/pytorch_models/mutable_scope.py
src/sdk/pynni/tests/models/pytorch_models/mutable_scope.py
+95
-0
src/sdk/pynni/tests/models/pytorch_models/naive.py
src/sdk/pynni/tests/models/pytorch_models/naive.py
+45
-0
src/sdk/pynni/tests/models/pytorch_models/nested.py
src/sdk/pynni/tests/models/pytorch_models/nested.py
+34
-0
src/sdk/pynni/tests/test_nas.py
src/sdk/pynni/tests/test_nas.py
+106
-0
test/config_test.py
test/config_test.py
+7
-1
test/generate_ts_config.py
test/generate_ts_config.py
+17
-2
test/pipelines-it-frameworkcontroller.yml
test/pipelines-it-frameworkcontroller.yml
+55
-0
test/pipelines-it-local-windows.yml
test/pipelines-it-local-windows.yml
+1
-1
test/training_service.yml
test/training_service.yml
+26
-0
No files found.
src/nni_manager/rest_server/restValidationSchemas.ts
View file @
e9f137f0
...
@@ -38,6 +38,7 @@ export namespace ValidationSchemas {
...
@@ -38,6 +38,7 @@ export namespace ValidationSchemas {
authFile
:
joi
.
string
(),
authFile
:
joi
.
string
(),
nniManagerNFSMountPath
:
joi
.
string
().
min
(
1
),
nniManagerNFSMountPath
:
joi
.
string
().
min
(
1
),
containerNFSMountPath
:
joi
.
string
().
min
(
1
),
containerNFSMountPath
:
joi
.
string
().
min
(
1
),
paiConfigPath
:
joi
.
string
(),
paiStoragePlugin
:
joi
.
string
().
min
(
1
),
paiStoragePlugin
:
joi
.
string
().
min
(
1
),
nasMode
:
joi
.
string
().
valid
(
'
classic_mode
'
,
'
enas_mode
'
,
'
oneshot_mode
'
,
'
darts_mode
'
),
nasMode
:
joi
.
string
().
valid
(
'
classic_mode
'
,
'
enas_mode
'
,
'
oneshot_mode
'
,
'
darts_mode
'
),
portList
:
joi
.
array
().
items
(
joi
.
object
({
portList
:
joi
.
array
().
items
(
joi
.
object
({
...
...
src/nni_manager/training_service/pai/paiK8S/paiK8SConfig.ts
View file @
e9f137f0
...
@@ -31,10 +31,11 @@ export class NNIPAIK8STrialConfig extends TrialConfig {
...
@@ -31,10 +31,11 @@ export class NNIPAIK8STrialConfig extends TrialConfig {
public
readonly
nniManagerNFSMountPath
:
string
;
public
readonly
nniManagerNFSMountPath
:
string
;
public
readonly
containerNFSMountPath
:
string
;
public
readonly
containerNFSMountPath
:
string
;
public
readonly
paiStoragePlugin
:
string
;
public
readonly
paiStoragePlugin
:
string
;
public
readonly
paiConfigPath
?:
string
;
constructor
(
command
:
string
,
codeDir
:
string
,
gpuNum
:
number
,
cpuNum
:
number
,
memoryMB
:
number
,
constructor
(
command
:
string
,
codeDir
:
string
,
gpuNum
:
number
,
cpuNum
:
number
,
memoryMB
:
number
,
image
:
string
,
nniManagerNFSMountPath
:
string
,
containerNFSMountPath
:
string
,
image
:
string
,
nniManagerNFSMountPath
:
string
,
containerNFSMountPath
:
string
,
paiStoragePlugin
:
string
,
virtualCluster
?:
string
)
{
paiStoragePlugin
:
string
,
virtualCluster
?:
string
,
paiConfigPath
?:
string
)
{
super
(
command
,
codeDir
,
gpuNum
);
super
(
command
,
codeDir
,
gpuNum
);
this
.
cpuNum
=
cpuNum
;
this
.
cpuNum
=
cpuNum
;
this
.
memoryMB
=
memoryMB
;
this
.
memoryMB
=
memoryMB
;
...
@@ -43,5 +44,6 @@ export class NNIPAIK8STrialConfig extends TrialConfig {
...
@@ -43,5 +44,6 @@ export class NNIPAIK8STrialConfig extends TrialConfig {
this
.
nniManagerNFSMountPath
=
nniManagerNFSMountPath
;
this
.
nniManagerNFSMountPath
=
nniManagerNFSMountPath
;
this
.
containerNFSMountPath
=
containerNFSMountPath
;
this
.
containerNFSMountPath
=
containerNFSMountPath
;
this
.
paiStoragePlugin
=
paiStoragePlugin
;
this
.
paiStoragePlugin
=
paiStoragePlugin
;
this
.
paiConfigPath
=
paiConfigPath
;
}
}
}
}
src/nni_manager/training_service/pai/paiK8S/paiK8STrainingService.ts
View file @
e9f137f0
...
@@ -44,6 +44,7 @@ import { PAIClusterConfig, PAITrialJobDetail } from '../paiConfig';
...
@@ -44,6 +44,7 @@ import { PAIClusterConfig, PAITrialJobDetail } from '../paiConfig';
import
{
PAIJobRestServer
}
from
'
../paiJobRestServer
'
;
import
{
PAIJobRestServer
}
from
'
../paiJobRestServer
'
;
const
yaml
=
require
(
'
js-yaml
'
);
const
yaml
=
require
(
'
js-yaml
'
);
const
deepmerge
=
require
(
'
deepmerge
'
);
/**
/**
* Training Service implementation for OpenPAI (Open Platform for AI)
* Training Service implementation for OpenPAI (Open Platform for AI)
...
@@ -59,6 +60,10 @@ class PAIK8STrainingService extends PAITrainingService {
...
@@ -59,6 +60,10 @@ class PAIK8STrainingService extends PAITrainingService {
public
async
setClusterMetadata
(
key
:
string
,
value
:
string
):
Promise
<
void
>
{
public
async
setClusterMetadata
(
key
:
string
,
value
:
string
):
Promise
<
void
>
{
switch
(
key
)
{
switch
(
key
)
{
case
TrialConfigMetadataKey
.
NNI_MANAGER_IP
:
this
.
nniManagerIpConfig
=
<
NNIManagerIpConfig
>
JSON
.
parse
(
value
);
break
;
case
TrialConfigMetadataKey
.
PAI_CLUSTER_CONFIG
:
case
TrialConfigMetadataKey
.
PAI_CLUSTER_CONFIG
:
this
.
paiJobRestServer
=
new
PAIJobRestServer
(
component
.
get
(
PAIK8STrainingService
));
this
.
paiJobRestServer
=
new
PAIJobRestServer
(
component
.
get
(
PAIK8STrainingService
));
this
.
paiClusterConfig
=
<
PAIClusterConfig
>
JSON
.
parse
(
value
);
this
.
paiClusterConfig
=
<
PAIClusterConfig
>
JSON
.
parse
(
value
);
...
@@ -185,8 +190,20 @@ class PAIK8STrainingService extends PAITrainingService {
...
@@ -185,8 +190,20 @@ class PAIK8STrainingService extends PAITrainingService {
}
}
}
}
if
(
this
.
paiTrialConfig
.
paiConfigPath
)
{
try
{
const
additionalPAIConfig
=
yaml
.
safeLoad
(
fs
.
readFileSync
(
this
.
paiTrialConfig
.
paiConfigPath
,
'
utf8
'
));
//deepmerge(x, y), if an element at the same key is present for both x and y, the value from y will appear in the result.
//refer: https://github.com/TehShrike/deepmerge
const
overwriteMerge
=
(
destinationArray
:
any
,
sourceArray
:
any
,
options
:
any
)
=>
sourceArray
;
return
yaml
.
safeDump
(
deepmerge
(
additionalPAIConfig
,
paiJobConfig
,
{
arrayMerge
:
overwriteMerge
}));
}
catch
(
error
)
{
this
.
log
.
error
(
`Error occurs during loading and merge
${
this
.
paiTrialConfig
.
paiConfigPath
}
:
${
error
}
`
);
}
}
else
{
return
yaml
.
safeDump
(
paiJobConfig
);
return
yaml
.
safeDump
(
paiJobConfig
);
}
}
}
protected
async
submitTrialJobToPAI
(
trialJobId
:
string
):
Promise
<
boolean
>
{
protected
async
submitTrialJobToPAI
(
trialJobId
:
string
):
Promise
<
boolean
>
{
const
deferred
:
Deferred
<
boolean
>
=
new
Deferred
<
boolean
>
();
const
deferred
:
Deferred
<
boolean
>
=
new
Deferred
<
boolean
>
();
...
@@ -254,7 +271,7 @@ class PAIK8STrainingService extends PAITrainingService {
...
@@ -254,7 +271,7 @@ class PAIK8STrainingService extends PAITrainingService {
this
.
log
.
info
(
`nniPAItrial command is
${
nniPaiTrialCommand
.
trim
()}
`
);
this
.
log
.
info
(
`nniPAItrial command is
${
nniPaiTrialCommand
.
trim
()}
`
);
const
paiJobConfig
=
this
.
generateJobConfigInYamlFormat
(
trialJobId
,
nniPaiTrialCommand
);
const
paiJobConfig
=
this
.
generateJobConfigInYamlFormat
(
trialJobId
,
nniPaiTrialCommand
);
this
.
log
.
debug
(
paiJobConfig
);
// Step 3. Submit PAI job via Rest call
// Step 3. Submit PAI job via Rest call
// Refer https://github.com/Microsoft/pai/blob/master/docs/rest-server/API.md for more detail about PAI Rest API
// Refer https://github.com/Microsoft/pai/blob/master/docs/rest-server/API.md for more detail about PAI Rest API
const
submitJobRequest
:
request
.
Options
=
{
const
submitJobRequest
:
request
.
Options
=
{
...
...
src/nni_manager/yarn.lock
View file @
e9f137f0
...
@@ -1112,6 +1112,11 @@ deepmerge@^2.1.1:
...
@@ -1112,6 +1112,11 @@ deepmerge@^2.1.1:
version "2.2.1"
version "2.2.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
deepmerge@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
default-require-extensions@^2.0.0:
default-require-extensions@^2.0.0:
version "2.0.0"
version "2.0.0"
resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7"
resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7"
...
...
src/sdk/pynni/nni/compression/torch/pruners.py
View file @
e9f137f0
...
@@ -113,7 +113,7 @@ class AGP_Pruner(Pruner):
...
@@ -113,7 +113,7 @@ class AGP_Pruner(Pruner):
if
k
==
0
or
target_sparsity
>=
1
or
target_sparsity
<=
0
:
if
k
==
0
or
target_sparsity
>=
1
or
target_sparsity
<=
0
:
return
mask
return
mask
# if we want to generate new mask, we should update weigth first
# if we want to generate new mask, we should update weigth first
w_abs
=
weight
.
abs
()
*
mask
w_abs
=
weight
.
abs
()
*
mask
[
'weight'
]
threshold
=
torch
.
topk
(
w_abs
.
view
(
-
1
),
k
,
largest
=
False
)[
0
].
max
()
threshold
=
torch
.
topk
(
w_abs
.
view
(
-
1
),
k
,
largest
=
False
)[
0
].
max
()
new_mask
=
{
'weight'
:
torch
.
gt
(
w_abs
,
threshold
).
type_as
(
weight
)}
new_mask
=
{
'weight'
:
torch
.
gt
(
w_abs
,
threshold
).
type_as
(
weight
)}
self
.
mask_dict
.
update
({
op_name
:
new_mask
})
self
.
mask_dict
.
update
({
op_name
:
new_mask
})
...
...
src/sdk/pynni/nni/medianstop_assessor/test.py
View file @
e9f137f0
...
@@ -31,11 +31,11 @@ def test():
...
@@ -31,11 +31,11 @@ def test():
# [1,1,1,1,1,1,1,1,1,1],
# [1,1,1,1,1,1,1,1,1,1],
# [1,1,1,1,1,1,1,1,1,1]]
# [1,1,1,1,1,1,1,1,1,1]]
assessor
=
MedianstopAssessor
(
FLAGS
.
start_step
,
FLAGS
.
optimize_mode
)
assessor
=
MedianstopAssessor
(
FLAGS
.
optimize_mode
,
FLAGS
.
start_step
)
for
i
in
range
(
4
):
for
i
in
range
(
len
(
lcs
)
):
#lc = []
#lc = []
to_complete
=
True
to_complete
=
True
for
k
in
range
(
10
):
for
k
in
range
(
len
(
lcs
[
0
])
):
#d = random.randint(i*100+0, i*100+100)
#d = random.randint(i*100+0, i*100+100)
#lc.append(d)
#lc.append(d)
ret
=
assessor
.
assess_trial
(
i
,
lcs
[
i
][:
k
+
1
])
ret
=
assessor
.
assess_trial
(
i
,
lcs
[
i
][:
k
+
1
])
...
...
src/sdk/pynni/nni/nas/pytorch/classic_nas/mutator.py
View file @
e9f137f0
...
@@ -68,6 +68,13 @@ class ClassicMutator(Mutator):
...
@@ -68,6 +68,13 @@ class ClassicMutator(Mutator):
else
:
else
:
# get chosen arch from tuner
# get chosen arch from tuner
self
.
_chosen_arch
=
nni
.
get_next_parameter
()
self
.
_chosen_arch
=
nni
.
get_next_parameter
()
if
self
.
_chosen_arch
is
None
:
if
trial_env_vars
.
NNI_PLATFORM
==
"unittest"
:
# happens if NNI_PLATFORM is intentionally set, e.g., in UT
logger
.
warning
(
"`NNI_PLATFORM` is set but `param` is None. Falling back to standalone mode."
)
self
.
_chosen_arch
=
self
.
_standalone_generate_chosen
()
else
:
raise
RuntimeError
(
"Chosen architecture is None. This may be a platform error."
)
self
.
reset
()
self
.
reset
()
def
_sample_layer_choice
(
self
,
mutable
,
idx
,
value
,
search_space_item
):
def
_sample_layer_choice
(
self
,
mutable
,
idx
,
value
,
search_space_item
):
...
@@ -169,6 +176,8 @@ class ClassicMutator(Mutator):
...
@@ -169,6 +176,8 @@ class ClassicMutator(Mutator):
elif
val
[
"_type"
]
==
INPUT_CHOICE
:
elif
val
[
"_type"
]
==
INPUT_CHOICE
:
choices
=
val
[
"_value"
][
"candidates"
]
choices
=
val
[
"_value"
][
"candidates"
]
n_chosen
=
val
[
"_value"
][
"n_chosen"
]
n_chosen
=
val
[
"_value"
][
"n_chosen"
]
if
n_chosen
is
None
:
n_chosen
=
len
(
choices
)
chosen_arch
[
key
]
=
{
"_value"
:
choices
[:
n_chosen
],
"_idx"
:
list
(
range
(
n_chosen
))}
chosen_arch
[
key
]
=
{
"_value"
:
choices
[:
n_chosen
],
"_idx"
:
list
(
range
(
n_chosen
))}
else
:
else
:
raise
ValueError
(
"Unknown key '%s' and value '%s'."
%
(
key
,
val
))
raise
ValueError
(
"Unknown key '%s' and value '%s'."
%
(
key
,
val
))
...
...
src/sdk/pynni/nni/nas/pytorch/darts/mutator.py
View file @
e9f137f0
...
@@ -63,7 +63,8 @@ class DartsMutator(Mutator):
...
@@ -63,7 +63,8 @@ class DartsMutator(Mutator):
edges_max
[
mutable
.
key
]
=
max_val
edges_max
[
mutable
.
key
]
=
max_val
result
[
mutable
.
key
]
=
F
.
one_hot
(
index
,
num_classes
=
mutable
.
length
).
view
(
-
1
).
bool
()
result
[
mutable
.
key
]
=
F
.
one_hot
(
index
,
num_classes
=
mutable
.
length
).
view
(
-
1
).
bool
()
for
mutable
in
self
.
mutables
:
for
mutable
in
self
.
mutables
:
if
isinstance
(
mutable
,
InputChoice
)
and
mutable
.
n_chosen
is
not
None
:
if
isinstance
(
mutable
,
InputChoice
):
if
mutable
.
n_chosen
is
not
None
:
weights
=
[]
weights
=
[]
for
src_key
in
mutable
.
choose_from
:
for
src_key
in
mutable
.
choose_from
:
if
src_key
not
in
edges_max
:
if
src_key
not
in
edges_max
:
...
@@ -74,7 +75,11 @@ class DartsMutator(Mutator):
...
@@ -74,7 +75,11 @@ class DartsMutator(Mutator):
selected_multihot
=
[]
selected_multihot
=
[]
for
i
,
src_key
in
enumerate
(
mutable
.
choose_from
):
for
i
,
src_key
in
enumerate
(
mutable
.
choose_from
):
if
i
not
in
topk_edge_indices
and
src_key
in
result
:
if
i
not
in
topk_edge_indices
and
src_key
in
result
:
result
[
src_key
]
=
torch
.
zeros_like
(
result
[
src_key
])
# clear this choice to optimize calc graph
# If an edge is never selected, there is no need to calculate any op on this edge.
# This is to eliminate redundant calculation.
result
[
src_key
]
=
torch
.
zeros_like
(
result
[
src_key
])
selected_multihot
.
append
(
i
in
topk_edge_indices
)
selected_multihot
.
append
(
i
in
topk_edge_indices
)
result
[
mutable
.
key
]
=
torch
.
tensor
(
selected_multihot
,
dtype
=
torch
.
bool
,
device
=
self
.
device
())
# pylint: disable=not-callable
result
[
mutable
.
key
]
=
torch
.
tensor
(
selected_multihot
,
dtype
=
torch
.
bool
,
device
=
self
.
device
())
# pylint: disable=not-callable
else
:
result
[
mutable
.
key
]
=
torch
.
ones
(
mutable
.
n_candidates
,
dtype
=
torch
.
bool
,
device
=
self
.
device
())
# pylint: disable=not-callable
return
result
return
result
src/sdk/pynni/nni/nas/pytorch/fixed.py
View file @
e9f137f0
...
@@ -58,16 +58,16 @@ def _encode_tensor(data):
...
@@ -58,16 +58,16 @@ def _encode_tensor(data):
return
data
return
data
def
apply_fixed_architecture
(
model
,
fixed_arc
_path
):
def
apply_fixed_architecture
(
model
,
fixed_arc
):
"""
"""
Load architecture from `fixed_arc
_path
` and apply to model.
Load architecture from `fixed_arc` and apply to model.
Parameters
Parameters
----------
----------
model : torch.nn.Module
model : torch.nn.Module
Model with mutables.
Model with mutables.
fixed_arc
_path
: str
fixed_arc : str
or dict
Path to the JSON that stores the architecture.
Path to the JSON that stores the
architecture, or dict that stores the exported
architecture.
Returns
Returns
-------
-------
...
@@ -75,8 +75,8 @@ def apply_fixed_architecture(model, fixed_arc_path):
...
@@ -75,8 +75,8 @@ def apply_fixed_architecture(model, fixed_arc_path):
Mutator that is responsible for fixes the graph.
Mutator that is responsible for fixes the graph.
"""
"""
if
isinstance
(
fixed_arc
_path
,
str
):
if
isinstance
(
fixed_arc
,
str
):
with
open
(
fixed_arc
_path
,
"r"
)
as
f
:
with
open
(
fixed_arc
)
as
f
:
fixed_arc
=
json
.
load
(
f
)
fixed_arc
=
json
.
load
(
f
)
fixed_arc
=
_encode_tensor
(
fixed_arc
)
fixed_arc
=
_encode_tensor
(
fixed_arc
)
architecture
=
FixedArchitecture
(
model
,
fixed_arc
)
architecture
=
FixedArchitecture
(
model
,
fixed_arc
)
...
...
src/sdk/pynni/nni/nas/pytorch/utils.py
View file @
e9f137f0
...
@@ -20,6 +20,14 @@ def global_mutable_counting():
...
@@ -20,6 +20,14 @@ def global_mutable_counting():
return
_counter
return
_counter
def
_reset_global_mutable_counting
():
"""
Reset the global mutable counting to count from 1. Useful when defining multiple models with default keys.
"""
global
_counter
_counter
=
0
def
to_device
(
obj
,
device
):
def
to_device
(
obj
,
device
):
"""
"""
Move a tensor, tuple, list, or dict onto device.
Move a tensor, tuple, list, or dict onto device.
...
...
src/sdk/pynni/tests/models/pytorch_models/__init__.py
0 → 100644
View file @
e9f137f0
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
from
.mutable_scope
import
SpaceWithMutableScope
from
.naive
import
NaiveSearchSpace
from
.nested
import
NestedSpace
src/sdk/pynni/tests/models/pytorch_models/mutable_scope.py
0 → 100644
View file @
e9f137f0
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
import
torch
import
torch.nn
as
nn
import
torch.nn.functional
as
F
from
nni.nas.pytorch.mutables
import
LayerChoice
,
InputChoice
,
MutableScope
class
Cell
(
MutableScope
):
def
__init__
(
self
,
cell_name
,
prev_labels
,
channels
):
super
().
__init__
(
cell_name
)
self
.
input_choice
=
InputChoice
(
choose_from
=
prev_labels
,
n_chosen
=
1
,
return_mask
=
True
,
key
=
cell_name
+
"_input"
)
self
.
op_choice
=
LayerChoice
([
nn
.
Conv2d
(
channels
,
channels
,
3
,
padding
=
1
),
nn
.
Conv2d
(
channels
,
channels
,
5
,
padding
=
2
),
nn
.
MaxPool2d
(
3
,
stride
=
1
,
padding
=
1
),
nn
.
AvgPool2d
(
3
,
stride
=
1
,
padding
=
1
),
nn
.
Identity
()
],
key
=
cell_name
+
"_op"
)
def
forward
(
self
,
prev_layers
):
chosen_input
,
chosen_mask
=
self
.
input_choice
(
prev_layers
)
cell_out
=
self
.
op_choice
(
chosen_input
)
return
cell_out
,
chosen_mask
class
Node
(
MutableScope
):
def
__init__
(
self
,
node_name
,
prev_node_names
,
channels
):
super
().
__init__
(
node_name
)
self
.
cell_x
=
Cell
(
node_name
+
"_x"
,
prev_node_names
,
channels
)
self
.
cell_y
=
Cell
(
node_name
+
"_y"
,
prev_node_names
,
channels
)
def
forward
(
self
,
prev_layers
):
out_x
,
mask_x
=
self
.
cell_x
(
prev_layers
)
out_y
,
mask_y
=
self
.
cell_y
(
prev_layers
)
return
out_x
+
out_y
,
mask_x
|
mask_y
class
Layer
(
nn
.
Module
):
def
__init__
(
self
,
num_nodes
,
channels
):
super
().
__init__
()
self
.
num_nodes
=
num_nodes
self
.
nodes
=
nn
.
ModuleList
()
node_labels
=
[
InputChoice
.
NO_KEY
,
InputChoice
.
NO_KEY
]
for
i
in
range
(
num_nodes
):
node_labels
.
append
(
"node_{}"
.
format
(
i
))
self
.
nodes
.
append
(
Node
(
node_labels
[
-
1
],
node_labels
[:
-
1
],
channels
))
self
.
final_conv_w
=
nn
.
Parameter
(
torch
.
zeros
(
channels
,
self
.
num_nodes
+
2
,
channels
,
1
,
1
),
requires_grad
=
True
)
self
.
bn
=
nn
.
BatchNorm2d
(
channels
,
affine
=
False
)
def
forward
(
self
,
pprev
,
prev
):
prev_nodes_out
=
[
pprev
,
prev
]
nodes_used_mask
=
torch
.
zeros
(
self
.
num_nodes
+
2
,
dtype
=
torch
.
bool
,
device
=
prev
.
device
)
for
i
in
range
(
self
.
num_nodes
):
node_out
,
mask
=
self
.
nodes
[
i
](
prev_nodes_out
)
nodes_used_mask
[:
mask
.
size
(
0
)]
|=
mask
.
to
(
prev
.
device
)
# NOTE: which device should we put mask on?
prev_nodes_out
.
append
(
node_out
)
unused_nodes
=
torch
.
cat
([
out
for
used
,
out
in
zip
(
nodes_used_mask
,
prev_nodes_out
)
if
not
used
],
1
)
unused_nodes
=
F
.
relu
(
unused_nodes
)
conv_weight
=
self
.
final_conv_w
[:,
~
nodes_used_mask
,
:,
:,
:]
conv_weight
=
conv_weight
.
view
(
conv_weight
.
size
(
0
),
-
1
,
1
,
1
)
out
=
F
.
conv2d
(
unused_nodes
,
conv_weight
)
return
prev
,
self
.
bn
(
out
)
class
SpaceWithMutableScope
(
nn
.
Module
):
def
__init__
(
self
,
test_case
,
num_layers
=
4
,
num_nodes
=
5
,
channels
=
16
,
in_channels
=
3
,
num_classes
=
10
):
super
().
__init__
()
self
.
test_case
=
test_case
self
.
num_layers
=
num_layers
self
.
stem
=
nn
.
Sequential
(
nn
.
Conv2d
(
in_channels
,
channels
,
3
,
1
,
1
,
bias
=
False
),
nn
.
BatchNorm2d
(
channels
)
)
self
.
layers
=
nn
.
ModuleList
()
for
_
in
range
(
self
.
num_layers
+
2
):
self
.
layers
.
append
(
Layer
(
num_nodes
,
channels
))
self
.
gap
=
nn
.
AdaptiveAvgPool2d
(
1
)
self
.
dense
=
nn
.
Linear
(
channels
,
num_classes
)
def
forward
(
self
,
x
):
prev
=
cur
=
self
.
stem
(
x
)
for
layer
in
self
.
layers
:
prev
,
cur
=
layer
(
prev
,
cur
)
cur
=
self
.
gap
(
F
.
relu
(
cur
)).
view
(
x
.
size
(
0
),
-
1
)
return
self
.
dense
(
cur
)
src/sdk/pynni/tests/models/pytorch_models/naive.py
0 → 100644
View file @
e9f137f0
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
import
torch
import
torch.nn
as
nn
import
torch.nn.functional
as
F
from
nni.nas.pytorch.mutables
import
LayerChoice
,
InputChoice
class
NaiveSearchSpace
(
nn
.
Module
):
def
__init__
(
self
,
test_case
):
super
().
__init__
()
self
.
test_case
=
test_case
self
.
conv1
=
LayerChoice
([
nn
.
Conv2d
(
3
,
6
,
3
,
padding
=
1
),
nn
.
Conv2d
(
3
,
6
,
5
,
padding
=
2
)])
self
.
pool
=
nn
.
MaxPool2d
(
2
,
2
)
self
.
conv2
=
LayerChoice
([
nn
.
Conv2d
(
6
,
16
,
3
,
padding
=
1
),
nn
.
Conv2d
(
6
,
16
,
5
,
padding
=
2
)],
return_mask
=
True
)
self
.
conv3
=
nn
.
Conv2d
(
16
,
16
,
1
)
self
.
skipconnect
=
InputChoice
(
n_candidates
=
1
)
self
.
skipconnect2
=
InputChoice
(
n_candidates
=
2
,
return_mask
=
True
)
self
.
bn
=
nn
.
BatchNorm2d
(
16
)
self
.
gap
=
nn
.
AdaptiveAvgPool2d
(
1
)
self
.
fc
=
nn
.
Linear
(
16
,
10
)
def
forward
(
self
,
x
):
bs
=
x
.
size
(
0
)
x
=
self
.
pool
(
F
.
relu
(
self
.
conv1
(
x
)))
x0
,
mask
=
self
.
conv2
(
x
)
self
.
test_case
.
assertEqual
(
mask
.
size
(),
torch
.
Size
([
2
]))
x1
=
F
.
relu
(
self
.
conv3
(
x0
))
_
,
mask
=
self
.
skipconnect2
([
x0
,
x1
])
x0
=
self
.
skipconnect
([
x0
])
if
x0
is
not
None
:
x1
+=
x0
x
=
self
.
pool
(
self
.
bn
(
x1
))
self
.
test_case
.
assertEqual
(
mask
.
size
(),
torch
.
Size
([
2
]))
x
=
self
.
gap
(
x
).
view
(
bs
,
-
1
)
x
=
self
.
fc
(
x
)
return
x
src/sdk/pynni/tests/models/pytorch_models/nested.py
0 → 100644
View file @
e9f137f0
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
import
torch.nn
as
nn
import
torch.nn.functional
as
F
from
nni.nas.pytorch.mutables
import
LayerChoice
,
InputChoice
class
MutableOp
(
nn
.
Module
):
def
__init__
(
self
,
kernel_size
):
super
().
__init__
()
self
.
conv
=
nn
.
Conv2d
(
3
,
120
,
kernel_size
,
padding
=
kernel_size
//
2
)
self
.
nested_mutable
=
InputChoice
(
n_candidates
=
10
)
def
forward
(
self
,
x
):
return
self
.
conv
(
x
)
class
NestedSpace
(
nn
.
Module
):
# this doesn't pass tests
def
__init__
(
self
,
test_case
):
super
().
__init__
()
self
.
test_case
=
test_case
self
.
conv1
=
LayerChoice
([
MutableOp
(
3
),
MutableOp
(
5
)])
self
.
gap
=
nn
.
AdaptiveAvgPool2d
(
1
)
self
.
fc1
=
nn
.
Linear
(
120
,
10
)
def
forward
(
self
,
x
):
bs
=
x
.
size
(
0
)
x
=
F
.
relu
(
self
.
conv1
(
x
))
x
=
self
.
gap
(
x
).
view
(
bs
,
-
1
)
x
=
self
.
fc
(
x
)
return
x
src/sdk/pynni/tests/test_nas.py
0 → 100644
View file @
e9f137f0
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
import
importlib
import
os
import
sys
from
unittest
import
TestCase
,
main
import
torch
import
torch.nn
as
nn
from
nni.nas.pytorch.classic_nas
import
get_and_apply_next_architecture
from
nni.nas.pytorch.darts
import
DartsMutator
from
nni.nas.pytorch.enas
import
EnasMutator
from
nni.nas.pytorch.fixed
import
apply_fixed_architecture
from
nni.nas.pytorch.random
import
RandomMutator
from
nni.nas.pytorch.utils
import
_reset_global_mutable_counting
class
NasTestCase
(
TestCase
):
def
setUp
(
self
):
self
.
default_input_size
=
[
3
,
32
,
32
]
self
.
model_path
=
os
.
path
.
join
(
os
.
path
.
dirname
(
__file__
),
"models"
)
sys
.
path
.
append
(
self
.
model_path
)
self
.
model_module
=
importlib
.
import_module
(
"pytorch_models"
)
self
.
default_cls
=
[
self
.
model_module
.
NaiveSearchSpace
,
self
.
model_module
.
SpaceWithMutableScope
]
self
.
cuda_test
=
[
0
]
if
torch
.
cuda
.
is_available
():
self
.
cuda_test
.
append
(
1
)
if
torch
.
cuda
.
device_count
()
>
1
:
self
.
cuda_test
.
append
(
torch
.
cuda
.
device_count
())
def
tearDown
(
self
):
sys
.
path
.
remove
(
self
.
model_path
)
def
iterative_sample_and_forward
(
self
,
model
,
mutator
=
None
,
input_size
=
None
,
n_iters
=
20
,
test_backward
=
True
,
use_cuda
=
False
):
if
input_size
is
None
:
input_size
=
self
.
default_input_size
# support pytorch only
input_size
=
[
8
if
use_cuda
else
2
]
+
input_size
# at least 2 samples to enable batch norm
for
_
in
range
(
n_iters
):
for
param
in
model
.
parameters
():
param
.
grad
=
None
if
mutator
is
not
None
:
mutator
.
reset
()
x
=
torch
.
randn
(
input_size
)
if
use_cuda
:
x
=
x
.
cuda
()
y
=
torch
.
sum
(
model
(
x
))
if
test_backward
:
y
.
backward
()
def
default_mutator_test_pipeline
(
self
,
mutator_cls
):
for
model_cls
in
self
.
default_cls
:
for
cuda_test
in
self
.
cuda_test
:
_reset_global_mutable_counting
()
model
=
model_cls
(
self
)
mutator
=
mutator_cls
(
model
)
if
cuda_test
:
model
.
cuda
()
mutator
.
cuda
()
if
cuda_test
>
1
:
model
=
nn
.
DataParallel
(
model
)
self
.
iterative_sample_and_forward
(
model
,
mutator
,
use_cuda
=
cuda_test
)
_reset_global_mutable_counting
()
model_fixed
=
model_cls
(
self
)
if
cuda_test
:
model_fixed
.
cuda
()
if
cuda_test
>
1
:
model_fixed
=
nn
.
DataParallel
(
model_fixed
)
with
torch
.
no_grad
():
arc
=
mutator
.
export
()
apply_fixed_architecture
(
model_fixed
,
arc
)
self
.
iterative_sample_and_forward
(
model_fixed
,
n_iters
=
1
,
use_cuda
=
cuda_test
)
def
test_random_mutator
(
self
):
self
.
default_mutator_test_pipeline
(
RandomMutator
)
def
test_enas_mutator
(
self
):
self
.
default_mutator_test_pipeline
(
EnasMutator
)
def
test_darts_mutator
(
self
):
# DARTS doesn't support DataParallel. To be fixed.
self
.
cuda_test
=
[
t
for
t
in
self
.
cuda_test
if
t
<=
1
]
self
.
default_mutator_test_pipeline
(
DartsMutator
)
def
test_apply_twice
(
self
):
model
=
self
.
model_module
.
NaiveSearchSpace
(
self
)
with
self
.
assertRaises
(
RuntimeError
):
for
_
in
range
(
2
):
RandomMutator
(
model
)
def
test_nested_space
(
self
):
model
=
self
.
model_module
.
NestedSpace
(
self
)
with
self
.
assertRaises
(
RuntimeError
):
RandomMutator
(
model
)
def
test_classic_nas
(
self
):
for
model_cls
in
self
.
default_cls
:
model
=
model_cls
(
self
)
get_and_apply_next_architecture
(
model
)
self
.
iterative_sample_and_forward
(
model
)
if
__name__
==
'__main__'
:
main
()
test/config_test.py
View file @
e9f137f0
...
@@ -30,6 +30,12 @@ def gen_new_config(config_file, training_service='local'):
...
@@ -30,6 +30,12 @@ def gen_new_config(config_file, training_service='local'):
if
'gpuNum'
in
config
[
'trial'
]:
if
'gpuNum'
in
config
[
'trial'
]:
config
[
'trial'
].
pop
(
'gpuNum'
)
config
[
'trial'
].
pop
(
'gpuNum'
)
if
training_service
==
'frameworkcontroller'
:
it_config
[
training_service
][
'trial'
][
'taskRoles'
][
0
][
'command'
]
=
config
[
'trial'
][
'command'
]
config
[
'trial'
].
pop
(
'command'
)
if
'gpuNum'
in
config
[
'trial'
]:
config
[
'trial'
].
pop
(
'gpuNum'
)
deep_update
(
config
,
it_config
[
'all'
])
deep_update
(
config
,
it_config
[
'all'
])
deep_update
(
config
,
it_config
[
training_service
])
deep_update
(
config
,
it_config
[
training_service
])
...
@@ -106,7 +112,7 @@ if __name__ == '__main__':
...
@@ -106,7 +112,7 @@ if __name__ == '__main__':
parser
=
argparse
.
ArgumentParser
()
parser
=
argparse
.
ArgumentParser
()
parser
.
add_argument
(
"--config"
,
type
=
str
,
default
=
None
)
parser
.
add_argument
(
"--config"
,
type
=
str
,
default
=
None
)
parser
.
add_argument
(
"--exclude"
,
type
=
str
,
default
=
None
)
parser
.
add_argument
(
"--exclude"
,
type
=
str
,
default
=
None
)
parser
.
add_argument
(
"--ts"
,
type
=
str
,
choices
=
[
'local'
,
'remote'
,
'pai'
,
'kubeflow'
],
default
=
'local'
)
parser
.
add_argument
(
"--ts"
,
type
=
str
,
choices
=
[
'local'
,
'remote'
,
'pai'
,
'kubeflow'
,
'frameworkcontroller'
],
default
=
'local'
)
parser
.
add_argument
(
"--local_gpu"
,
action
=
'store_true'
)
parser
.
add_argument
(
"--local_gpu"
,
action
=
'store_true'
)
parser
.
add_argument
(
"--preinstall"
,
action
=
'store_true'
)
parser
.
add_argument
(
"--preinstall"
,
action
=
'store_true'
)
args
=
parser
.
parse_args
()
args
=
parser
.
parse_args
()
...
...
test/generate_ts_config.py
View file @
e9f137f0
...
@@ -42,6 +42,21 @@ def update_training_service_config(args):
...
@@ -42,6 +42,21 @@ def update_training_service_config(args):
config
[
args
.
ts
][
'kubeflowConfig'
][
'azureStorage'
][
'azureShare'
]
=
args
.
azs_share
config
[
args
.
ts
][
'kubeflowConfig'
][
'azureStorage'
][
'azureShare'
]
=
args
.
azs_share
if
args
.
nni_docker_image
is
not
None
:
if
args
.
nni_docker_image
is
not
None
:
config
[
args
.
ts
][
'trial'
][
'worker'
][
'image'
]
=
args
.
nni_docker_image
config
[
args
.
ts
][
'trial'
][
'worker'
][
'image'
]
=
args
.
nni_docker_image
elif
args
.
ts
==
'frameworkcontroller'
:
if
args
.
nfs_server
is
not
None
:
config
[
args
.
ts
][
'frameworkcontrollerConfig'
][
'nfs'
][
'server'
]
=
args
.
nfs_server
if
args
.
nfs_path
is
not
None
:
config
[
args
.
ts
][
'frameworkcontrollerConfig'
][
'nfs'
][
'path'
]
=
args
.
nfs_path
if
args
.
keyvault_vaultname
is
not
None
:
config
[
args
.
ts
][
'frameworkcontrollerConfig'
][
'keyVault'
][
'vaultName'
]
=
args
.
keyvault_vaultname
if
args
.
keyvault_name
is
not
None
:
config
[
args
.
ts
][
'frameworkcontrollerConfig'
][
'keyVault'
][
'name'
]
=
args
.
keyvault_name
if
args
.
azs_account
is
not
None
:
config
[
args
.
ts
][
'frameworkcontrollerConfig'
][
'azureStorage'
][
'accountName'
]
=
args
.
azs_account
if
args
.
azs_share
is
not
None
:
config
[
args
.
ts
][
'frameworkcontrollerConfig'
][
'azureStorage'
][
'azureShare'
]
=
args
.
azs_share
if
args
.
nni_docker_image
is
not
None
:
config
[
args
.
ts
][
'trial'
][
'taskRoles'
][
0
][
'image'
]
=
args
.
nni_docker_image
elif
args
.
ts
==
'remote'
:
elif
args
.
ts
==
'remote'
:
if
args
.
remote_user
is
not
None
:
if
args
.
remote_user
is
not
None
:
config
[
args
.
ts
][
'machineList'
][
0
][
'username'
]
=
args
.
remote_user
config
[
args
.
ts
][
'machineList'
][
0
][
'username'
]
=
args
.
remote_user
...
@@ -69,7 +84,7 @@ def convert_command():
...
@@ -69,7 +84,7 @@ def convert_command():
if
__name__
==
'__main__'
:
if
__name__
==
'__main__'
:
parser
=
argparse
.
ArgumentParser
()
parser
=
argparse
.
ArgumentParser
()
parser
.
add_argument
(
"--ts"
,
type
=
str
,
choices
=
[
'pai'
,
'kubeflow'
,
'remote'
,
'local'
],
default
=
'pai'
)
parser
.
add_argument
(
"--ts"
,
type
=
str
,
choices
=
[
'pai'
,
'kubeflow'
,
'remote'
,
'local'
,
'frameworkcontroller'
],
default
=
'pai'
)
parser
.
add_argument
(
"--nni_docker_image"
,
type
=
str
)
parser
.
add_argument
(
"--nni_docker_image"
,
type
=
str
)
parser
.
add_argument
(
"--nni_manager_ip"
,
type
=
str
)
parser
.
add_argument
(
"--nni_manager_ip"
,
type
=
str
)
# args for PAI
# args for PAI
...
@@ -79,7 +94,7 @@ if __name__ == '__main__':
...
@@ -79,7 +94,7 @@ if __name__ == '__main__':
parser
.
add_argument
(
"--data_dir"
,
type
=
str
)
parser
.
add_argument
(
"--data_dir"
,
type
=
str
)
parser
.
add_argument
(
"--output_dir"
,
type
=
str
)
parser
.
add_argument
(
"--output_dir"
,
type
=
str
)
parser
.
add_argument
(
"--vc"
,
type
=
str
)
parser
.
add_argument
(
"--vc"
,
type
=
str
)
# args for kubeflow
# args for kubeflow
and frameworkController
parser
.
add_argument
(
"--nfs_server"
,
type
=
str
)
parser
.
add_argument
(
"--nfs_server"
,
type
=
str
)
parser
.
add_argument
(
"--nfs_path"
,
type
=
str
)
parser
.
add_argument
(
"--nfs_path"
,
type
=
str
)
parser
.
add_argument
(
"--keyvault_vaultname"
,
type
=
str
)
parser
.
add_argument
(
"--keyvault_vaultname"
,
type
=
str
)
...
...
test/pipelines-it-frameworkcontroller.yml
0 → 100644
View file @
e9f137f0
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
jobs
:
-
job
:
'
integration_test_frameworkController'
timeoutInMinutes
:
0
steps
:
-
script
:
python3 -m pip install --upgrade pip setuptools --user
displayName
:
'
Install
python
tools'
-
script
:
|
cd deployment/pypi
echo 'building prerelease package...'
make build
ls $(Build.SourcesDirectory)/deployment/pypi/dist/
condition
:
eq( variables['build_docker_img'], 'true' )
displayName
:
'
build
nni
bdsit_wheel'
-
script
:
|
source install.sh
displayName
:
'
Install
nni
toolkit
via
source
code'
-
script
:
|
sudo apt-get install swig -y
PATH=$HOME/.local/bin:$PATH nnictl package install --name=SMAC
PATH=$HOME/.local/bin:$PATH nnictl package install --name=BOHB
displayName
:
'
Install
dependencies
for
integration
tests
in
frameworkcontroller
mode'
-
script
:
|
if [ $(build_docker_img) = 'true' ]
then
cd deployment/pypi
docker login -u $(docker_hub_user) -p $(docker_hub_pwd)
echo 'updating docker file for installing nni from local...'
# update Dockerfile to install NNI in docker image from whl file built in last step
sed -ie 's/RUN python3 -m pip --no-cache-dir install nni/COPY .\/dist\/* .\nRUN python3 -m pip install nni-*.whl/' ../docker/Dockerfile
cat ../docker/Dockerfile
export IMG_TAG=`date -u +%y%m%d%H%M`
docker build -f ../docker/Dockerfile -t $(test_docker_img_name):$IMG_TAG .
docker push $(test_docker_img_name):$IMG_TAG
export TEST_IMG=$(test_docker_img_name):$IMG_TAG
cd ../../
else
export TEST_IMG=$(existing_docker_img)
fi
echo "TEST_IMG:$TEST_IMG"
cd test
python3 generate_ts_config.py --ts frameworkcontroller --keyvault_vaultname $(keyVault_vaultName) --keyvault_name $(keyVault_name) \
--azs_account $(azureStorage_accountName) --azs_share $(azureStorage_azureShare) --nni_docker_image $TEST_IMG --nni_manager_ip $(nni_manager_ip)
cat training_service.yml
PATH=$HOME/.local/bin:$PATH python3 config_test.py --ts frameworkcontroller --exclude multi_phase
displayName
:
'
integration
test'
test/pipelines-it-local-windows.yml
View file @
e9f137f0
...
@@ -8,7 +8,7 @@ jobs:
...
@@ -8,7 +8,7 @@ jobs:
-
script
:
|
-
script
:
|
python -m pip install scikit-learn==0.20.0 --user
python -m pip install scikit-learn==0.20.0 --user
python -m pip install keras==2.1.6 --user
python -m pip install keras==2.1.6 --user
python -m pip install
https://download.pytorch.org/whl/cu90/torch-0.4.1-cp36-cp36m-win_amd64.wh
l --user
python -m pip install
torch===1.2.0 torchvision===0.4.1 -f https://download.pytorch.org/whl/torch_stable.htm
l --user
python -m pip install torchvision --user
python -m pip install torchvision --user
python -m pip install tensorflow-gpu==1.11.0 --user
python -m pip install tensorflow-gpu==1.11.0 --user
displayName
:
'
Install
dependencies
for
integration
tests'
displayName
:
'
Install
dependencies
for
integration
tests'
...
...
test/training_service.yml
View file @
e9f137f0
...
@@ -24,6 +24,32 @@ kubeflow:
...
@@ -24,6 +24,32 @@ kubeflow:
image
:
image
:
trainingServicePlatform
:
kubeflow
trainingServicePlatform
:
kubeflow
frameworkcontroller
:
maxExecDuration
:
15m
nniManagerIp
:
frameworkcontrollerConfig
:
serviceAccountName
:
frameworkbarrier
storage
:
azureStorage
keyVault
:
vaultName
:
name
:
azureStorage
:
accountName
:
azureShare
:
trial
:
taskRoles
:
-
name
:
worker
taskNum
:
1
command
:
gpuNum
:
1
cpuNum
:
1
memoryMB
:
8192
image
:
frameworkAttemptCompletionPolicy
:
minFailedTaskCount
:
1
minSucceededTaskCount
:
1
trainingServicePlatform
:
frameworkcontroller
local
:
local
:
trainingServicePlatform
:
local
trainingServicePlatform
:
local
pai
:
pai
:
...
...
Prev
1
2
3
4
5
6
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment