1. 程式人生 > >Tensorflow原始碼解析6 -- TensorFlow本地執行時

Tensorflow原始碼解析6 -- TensorFlow本地執行時

Tensorflow原始碼解讀系列文章,歡迎閱讀
帶你深入AI(1) - 深度學習模型訓練痛點及解決方法
自然語言處理1 – 分詞
Tensorflow原始碼解析1 – 核心架構和原始碼結構
Tensorflow原始碼解析2 – 前後端連線的橋樑 - Session
Tensorflow原始碼解析3 – TensorFlow核心物件 - Graph
Tensorflow原始碼解析4 – 圖的節點 - Operation
Tensorflow原始碼解析5 – 圖的邊 - Tensor
Tensorflow原始碼解析6 – TensorFlow本地執行時
Tensorflow原始碼解析7 – TensorFlow分散式執行時

1 概述

TensorFlow後端分為四層,執行時層、計算層、通訊層、裝置層。執行時作為第一層,實現了session管理、graph管理等很多重要的邏輯,是十分關鍵的一層。根據任務分佈的不同,執行時又分為本地執行時和分散式執行時。本地執行時,所有任務運行於本地同一程序內。而分散式執行時,則允許任務執行在不同機器上。

Tensorflow的執行,通過session搭建了前後端溝通的橋樑,前端幾乎所有操作都是通過session進行。session的生命週期由建立、執行、關閉、銷燬組成,前文已經詳細講述過。可以將session看做TensorFlow執行的載體。而TensorFlow執行的核心物件,則是計算圖Graph。它由計算運算元和計算資料兩部分構成,可以完整描述整個計算內容。Graph的生命週期包括構建和傳遞、剪枝、分裂、執行等步驟,本文會詳細講解。理解TensorFlow的執行時,重點就是理解會話session和計算圖Graph。

本地執行時,client master和worker都在本地機器的同一程序內,均通過DirectSession類來描述。由於在同一程序內,三者間可以共享記憶體,通過DirectSession的相關函式實現呼叫。

client前端直接面向用戶,負責session的建立,計算圖Graph的構造。並通過session.run()將Graph序列化後傳遞給master。master收到後,先反序列化得到Graph,然後根據反向依賴關係,得到幾個最小依賴子圖,這一步稱為剪枝。之後master根據可執行的裝置情況,將子圖分裂到不同裝置上,從而可以併發執行,這一步稱為分裂。最後,由每個裝置上的worker並行執行分裂後的子圖,得到計算結果後返回。

2 Graph構建和傳遞

session.run()開啟了後端Graph的構建和傳遞。在前文session生命週期的講解中,session.run()時會先呼叫_extend_graph()將要執行的Operation新增到Graph中,然後再啟動執行過程。extend_graph()會先將graph序列化,得到graph_def,然後呼叫後端的TF_ExtendGraph()方法。下面我們從c_api.cc中的TF_ExtendGraph()看起。

// 增加節點到graph中,proto為序列化後的graph
void TF_ExtendGraph(TF_DeprecatedSession* s, const void* proto,
                    size_t proto_len, TF_Status* status) {
  GraphDef g;
  // 先將proto轉換為GrapDef。graphDef是圖的序列化表示,反序列化在後面。
  if (!tensorflow::ParseProtoUnlimited(&g, proto, proto_len)) {
    status->status = InvalidArgument("Invalid GraphDef");
    return;
  }

  // 再呼叫session的extend方法。根據建立的不同session型別,多型呼叫不同方法。
  status->status = s->session->Extend(g);
}

後端系統根據生成的Session型別,多型的呼叫Extend方法。如果是本地session,則呼叫DirectSession的Extend()方法。下面看DirectSession的Extend()方法。

Status DirectSession::Extend(const GraphDef& graph) {
  // 保證執行緒安全,然後呼叫ExtendLocked()
  mutex_lock l(graph_def_lock_);
  return ExtendLocked(graph);
}

// 主要任務就是建立GraphExecutionState物件。
Status DirectSession::ExtendLocked(const GraphDef& graph) {
  bool already_initialized;

  if (already_initialized) {
    TF_RETURN_IF_ERROR(flib_def_->AddLibrary(graph.library()));

    // 建立GraphExecutionState
    std::unique_ptr<GraphExecutionState> state;
    TF_RETURN_IF_ERROR(execution_state_->Extend(graph, &state));
    execution_state_.swap(state);
  }
  return Status::OK();
}

最終建立了GraphExecutionState物件。它主要工作有

  1. 負責將GraphDef反序列化為graph,從而構造出graph。在初始化方法InitBaseGraph()中
  2. 執行部分op編排工作,在初始化方法InitBaseGraph()中
Status GraphExecutionState::InitBaseGraph(const BuildGraphOptions& options) {
  const GraphDef* graph_def = &original_graph_def_;

  // graphDef反序列化得到graph
  std::unique_ptr<Graph> new_graph(new Graph(OpRegistry::Global()));
  GraphConstructorOptions opts;
  TF_RETURN_IF_ERROR(ConvertGraphDefToGraph(opts, *graph_def, new_graph.get()));

  // 恢復有狀態的節點
  RestoreStatefulNodes(new_graph.get());

  // 構造優化器的選項 optimization_options
  GraphOptimizationPassOptions optimization_options;
  optimization_options.session_options = session_options_;
  optimization_options.graph = &new_graph;
  optimization_options.flib_def = flib_def_.get();
  optimization_options.device_set = device_set_;

  TF_RETURN_IF_ERROR(OptimizationPassRegistry::Global()->RunGrouping(
      OptimizationPassRegistry::PRE_PLACEMENT, optimization_options));

  // plaer執行op編排
  Placer placer(new_graph.get(), device_set_, session_options_);
  TF_RETURN_IF_ERROR(placer.Run());

  TF_RETURN_IF_ERROR(OptimizationPassRegistry::Global()->RunGrouping(
      OptimizationPassRegistry::POST_PLACEMENT, optimization_options));

  // 報春狀態節點
  SaveStatefulNodes(new_graph.get());
  graph_ = new_graph.release();
  return Status::OK();
}

構造Graph:反序列化GraphDef為Graph

由於client傳遞給master的是序列化後的計算圖,所以master需要先反序列化。通過ConvertGraphDefToGraph實現。程式碼在graph_constructor.cc中,如下

Status ConvertGraphDefToGraph(const GraphConstructorOptions& opts,
                              const GraphDef& gdef, Graph* g) {
  ShapeRefiner refiner(gdef.versions().producer(), g->op_registry());
  return GraphConstructor::Construct(
      opts, gdef.node(), &gdef.versions(), &gdef.library(), g, &refiner,
      /*return_tensors=*/nullptr, /*return_nodes=*/nullptr,
      /*missing_unused_input_map_keys=*/nullptr);
}

編排OP

Operation編排的目的是,將op以最高效的方式,放在合適的硬體裝置上,從而最大限度的發揮硬體能力。通過Placer的run()方法進行,演算法很複雜,在placer.cc中,我也看得不大懂,就不展開了。

3 Graph剪枝

反序列化構建好Graph,並進行了Operation編排後,master就開始對Graph剪枝了。剪枝就是根據Graph的輸入輸出列表,反向遍歷全圖,找到幾個最小依賴的子圖,從而方便平行計算。

Status GraphExecutionState::BuildGraph(const BuildGraphOptions& options,
                                       std::unique_ptr<ClientGraph>* out) {

  std::unique_ptr<Graph> ng;
  Status s = OptimizeGraph(options, &ng);
  if (!s.ok()) {
    // 1 複製一份原始的Graph
    ng.reset(new Graph(flib_def_.get()));
    CopyGraph(*graph_, ng.get());
  }

  // 2 剪枝,根據輸入輸出feed fetch,對graph進行增加節點或刪除節點等操作。通過RewriteGraphForExecution()方法
  subgraph::RewriteGraphMetadata rewrite_metadata;
  if (session_options_ == nullptr ||
      !session_options_->config.graph_options().place_pruned_graph()) {
    TF_RETURN_IF_ERROR(subgraph::RewriteGraphForExecution(
        ng.get(), options.feed_endpoints, options.fetch_endpoints,
        options.target_nodes, device_set_->client_device()->attributes(),
        options.use_function_convention, &rewrite_metadata));
  }

  // 3 處理優化選項optimization_options
  GraphOptimizationPassOptions optimization_options;
  optimization_options.session_options = session_options_;
  optimization_options.graph = &ng;
  optimization_options.flib_def = flib.get();
  optimization_options.device_set = device_set_;

  TF_RETURN_IF_ERROR(OptimizationPassRegistry::Global()->RunGrouping(
      OptimizationPassRegistry::POST_REWRITE_FOR_EXEC, optimization_options));

  // 4 複製一份ClientGraph
  std::unique_ptr<ClientGraph> dense_copy(
      new ClientGraph(std::move(flib), rewrite_metadata.feed_types,
                      rewrite_metadata.fetch_types));
  CopyGraph(*ng, &dense_copy->graph);

  *out = std::move(dense_copy);
  return Status::OK();
}

剪枝的關鍵在RewriteGraphForExecution()方法中,在subgraph.cc檔案中。

Status RewriteGraphForExecution(
    Graph* g, const gtl::ArraySlice<string>& fed_outputs,
    const gtl::ArraySlice<string>& fetch_outputs,
    const gtl::ArraySlice<string>& target_node_names,
    const DeviceAttributes& device_info, bool use_function_convention,
    RewriteGraphMetadata* out_metadata) {

  std::unordered_set<string> endpoints;

  // 1 構建節點的name_index,從而快速索引節點。為FeedInputs,FetchOutputs等步驟所使用
  NameIndex name_index;
  name_index.reserve(g->num_nodes());
  for (Node* n : g->nodes()) {
    name_index[n->name()] = n;
  }

  // 2 FeedInputs,新增輸入節點
  if (!fed_outputs.empty()) {
    FeedInputs(g, device_info, fed_outputs, use_function_convention, &name_index, &out_metadata->feed_types);
  }

  // 3 FetchOutputs,新增輸出節點
  std::vector<Node*> fetch_nodes;
  if (!fetch_outputs.empty()) {
    FetchOutputs(g, device_info, fetch_outputs, use_function_convention, &name_index, &fetch_nodes, &out_metadata->fetch_types);
  }

  // 4 剪枝,形成若干最小依賴子圖
  if (!fetch_nodes.empty() || !target_node_names.empty()) {
    PruneForTargets(g, name_index, fetch_nodes, target_node_names);
  }

  return Status::OK();
}

主要有4步

  1. 構建節點的name_index,從而快速索引節點。為FeedInputs,FetchOutputs等步驟所使用
  2. FeedInputs,新增輸入節點。輸入節點的資料來源於session.run()時的feed列表。
  3. FetchOutputs,新增輸出節點。輸出節點在session.run()時通過fetches所給出
  4. 剪枝PruneForTargets,形成若干最小依賴子圖。這是剪枝演算法最關鍵的一步。

PruneForTargets()從輸出節點反向搜尋,按照BFS廣度優先演算法,找到若干個最小依賴子圖。

static Status PruneForTargets(Graph* g, const subgraph::NameIndex& name_index,
                              const std::vector<Node*>& fetch_nodes,
                              const gtl::ArraySlice<string>& target_nodes) {
  string not_found;
  std::unordered_set<const Node*> targets;

  // 1 AddNodeToTargets新增節點到targets中,從輸出節點按照BFS反向遍歷。
  for (Node* n : fetch_nodes) {
    AddNodeToTargets(n->name(), name_index, &targets);
  }

  // 2 剪枝,得到多個最小依賴子圖子圖
  PruneForReverseReachability(g, targets);

  // 修正Source和Sink節點的依賴邊,將沒有輸出邊的節點連線到sink node上
  FixupSourceAndSinkEdges(g);

  return Status::OK();
}

主要有3步

  1. AddNodeToTargets,從輸出節點按照BFS反向遍歷圖的節點,新增到targets中。
  2. PruneForReverseReachability,剪枝,得到多個最小依賴子圖子圖
  3. FixupSourceAndSinkEdges,修正Source和Sink節點的依賴邊,將沒有輸出邊的節點連線到sink node上

PruneForReverseReachability()在algorithm.cc檔案中,演算法就不分析了,總體是按照BFS廣度優先演算法搜尋的。

bool PruneForReverseReachability(Graph* g,
                                 std::unordered_set<const Node*> visited) {
  // 按照BFS廣度優先演算法,從輸出節點開始,反向搜尋節點的依賴關係
  std::deque<const Node*> queue;
  for (const Node* n : visited) {
    queue.push_back(n);
  }
  while (!queue.empty()) {
    const Node* n = queue.front();
    queue.pop_front();
    for (const Node* in : n->in_nodes()) {
      if (visited.insert(in).second) {
        queue.push_back(in);
      }
    }
  }

  // 刪除不在"visited"列表中的節點,說明最小依賴子圖不依賴此節點
  std::vector<Node*> all_nodes;
  all_nodes.reserve(g->num_nodes());
  for (Node* n : g->nodes()) {
    all_nodes.push_back(n);
  }

  bool any_removed = false;
  for (Node* n : all_nodes) {
    if (visited.count(n) == 0 && !n->IsSource() && !n->IsSink()) {
      g->RemoveNode(n);
      any_removed = true;
    }
  }

  return any_removed;
}

4 Graph分裂

剪枝完成後,master即得到了最小依賴子圖ClientGraph。然後根據本地機器的硬體裝置,以及op所指定的執行裝置等關係,將圖分裂為多個Partition Graph,傳遞到相關裝置的worker上,從而進行並行運算。這就是Graph的分裂。

Graph分裂的演算法在graph_partition.cc的Partition()方法中。演算法比較複雜,我們就不分析了。圖分裂有兩種

  1. splitbydevice按裝置分裂,也就是將Graph分裂到本地各CPU GPU上。本地執行時只使用按裝置分裂。

    static string SplitByDevice(const Node* node) {
      return node->assigned_device_name();
    }
    
  2. splitByWorker 按worker分裂, 也就是將Graph分裂到各分散式任務上,常用於分散式執行時。分散式執行時,圖會經歷兩次分裂。先splitByWorker分裂到各分散式任務上,一般是各分散式機器。然後splitbydevice二次分裂到分散式機器的CPU GPU等裝置上。

    static string SplitByWorker(const Node* node) {
      string task;
      string device;
      DeviceNameUtils::SplitDeviceName(node->assigned_device_name(), &task, &device);
      return task;
    }
    

5 Graph執行

Graph經過master剪枝和分裂後,就可以在本地的各CPU GPU裝置上執行了。這個過程的管理者叫worker。一般一個worker對應一個分裂後的子圖partitionGraph。每個worker啟動一個執行器Executor,入度為0的節點資料依賴已經ready了,故可以並行執行。等所有Executor執行完畢後,通知執行完畢。

各CPU GPU裝置間可能需要資料通訊,通過建立send/recv節點來解決。資料傳送方建立send節點,將資料放在send節點內,不阻塞。資料接收方建立recv節點,從recv節點中取出資料,recv節點中如果沒有資料則阻塞。這又是一個典型的生產者-消費者關係。

Graph執行的程式碼邏輯在direct_session.cc檔案的DirectSession::Run()方法中。程式碼邏輯很長,我們抽取其中的關鍵部分。

Status DirectSession::Run(const RunOptions& run_options,
                          const NamedTensorList& inputs,
                          const std::vector<string>& output_names,
                          const std::vector<string>& target_nodes,
                          std::vector<Tensor>* outputs,
                          RunMetadata* run_metadata) {

  // 1 將輸入tensor的name取出,組成一個列表,方便之後快速索引輸入tensor
  std::vector<string> input_tensor_names;
  input_tensor_names.reserve(inputs.size());
  for (const auto& it : inputs) {
    input_tensor_names.push_back(it.first);
  }

  // 2 傳遞輸入資料給executor,通過FunctionCallFrame方式。
  // 2.1 建立FunctionCallFrame,用來輸入資料給executor,並從executor中取出資料。
  FunctionCallFrame call_frame(executors_and_keys->input_types,
                               executors_and_keys->output_types);
  // 2.2 構造輸入資料feed_args
  gtl::InlinedVector<Tensor, 4> feed_args(inputs.size());
  for (const auto& it : inputs) {
    if (it.second.dtype() == DT_RESOURCE) {
      Tensor tensor_from_handle;
      ResourceHandleToInputTensor(it.second, &tensor_from_handle);
      feed_args[executors_and_keys->input_name_to_index[it.first]] = tensor_from_handle;
    } else {
      feed_args[executors_and_keys->input_name_to_index[it.first]] = it.second;
    }
  }

  // 2.3 將feed_args輸入資料設定到Arg節點上
  const Status s = call_frame.SetArgs(feed_args);


  // 3 開始執行executor
  // 3.1 建立run_state, 和IntraProcessRendezvous
  RunState run_state(args.step_id, &devices_);
  run_state.rendez = new IntraProcessRendezvous(device_mgr_.get());
  CancellationManager step_cancellation_manager;
  args.call_frame = &call_frame;

  // 3.2 建立ExecutorBarrier,它是一個執行完成的計數器。同時註冊執行完成的監聽事件executors_done.Notify()
  const size_t num_executors = executors_and_keys->items.size();
  ExecutorBarrier* barrier = new ExecutorBarrier(
      num_executors, run_state.rendez, [&run_state](const Status& ret) {
        {
          mutex_lock l(run_state.mu_);
          run_state.status.Update(ret);
        }
        // 所有執行緒池計算完畢後,會觸發Notify,傳送訊息。
        run_state.executors_done.Notify();
      });

  args.rendezvous = run_state.rendez;
  args.cancellation_manager = &step_cancellation_manager;
  args.session_state = &session_state_;
  args.tensor_store = &run_state.tensor_store;
  args.step_container = &run_state.step_container;
  args.sync_on_finish = sync_on_finish_;

  // 3.3 建立executor的執行器Runner
  Executor::Args::Runner default_runner = [this,
                                           pool](Executor::Args::Closure c) {
    SchedClosure(pool, std::move(c))
            
           

相關推薦

Tensorflow原始碼解析6 -- TensorFlow本地執行

Tensorflow原始碼解讀系列文章,歡迎閱讀 帶你深入AI(1) - 深度學習模型訓練痛點及解決方法 自然語言處理1 – 分詞 Tensorflow原始碼解析1 – 核心架構和原始碼結構 Tensorflow原始碼解析2 – 前後端連線的橋樑 - Session Tensorflow

Tensorflow原始碼解析7 -- TensorFlow分散式執行

Tensorflow原始碼解讀系列文章,歡迎閱讀 帶你深入AI(1) - 深度學習模型訓練痛點及解決方法 自然語言處理1 – 分詞 Tensorflow原始碼解析1 – 核心架構和原始碼結構 Tensorflow原始碼解析2 – 前後端連線的橋樑 - Session Tensorflow

Tensorflow原始碼解析3 -- TensorFlow核心物件 - Graph

Tensorflow原始碼解讀系列文章,歡迎閱讀 帶你深入AI(1) - 深度學習模型訓練痛點及解決方法 自然語言處理1 – 分詞 Tensorflow原始碼解析1 – 核心架構和原始碼結構 Tensorflow原始碼解析2 – 前後端連線的橋樑 - Session Tensorflow

Bi-LSTM-CRF(一)--tensorflow原始碼解析

1.1.核心程式碼: cell_fw = tf.contrib.rnn.LSTMCell(num_units=100) cell_bw = tf.contrib.rnn.LSTMCell(num_units=100) (outputs, output_states) =

Tensorflow原始碼解析1 -- 核心架構和原始碼結構

1 主流深度學習框架對比 當今的軟體開發基本都是分層化和模組化的,應用層開發會基於框架層。比如開發Linux Driver會基於Linux kernel,開發Android app會基於Android Framework。深度學習也不例外,框架層為上層模型開發提

Tensorflow原始碼解析4 -- 圖的節點Operation

1 概述 上文講述了TensorFlow的核心物件,計算圖Graph。Graph包含兩大成員,節點和邊。節點即為計算運算元Operation,邊則為計算資料Tensor。由起始節點Source出發,按照Graph的拓撲順序,依次執行節點的計算,即可完成整圖的計算

學習筆記TF050:TensorFlow原始碼解析

TensorFlow目錄結構。 ACKNOWLEDGMENTS #TensorFlow版本宣告 ADOPTERS.md #使用TensorFlow的人員或組織列表 AUTHORS #TensorFlow作者的官方列表 BUILD CONTRIBUTING.md

Tensorflow原始碼解析5 -- 圖的邊 - Tensor

Tensorflow原始碼解讀系列文章,歡迎閱讀 帶你深入AI(1) - 深度學習模型訓練痛點及解決方法 自然語言處理1 – 分詞 Tensorflow原始碼解析1 – 核心架構和原始碼結構 Tensorflow原始碼解析2 – 前後端連線的橋樑 - Session Tensorflow

Tensorflow原始碼解析4 -- 圖的節點 - Operation

Tensorflow原始碼解讀系列文章,歡迎閱讀 帶你深入AI(1) - 深度學習模型訓練痛點及解決方法 自然語言處理1 – 分詞 Tensorflow原始碼解析1 – 核心架構和原始碼結構 Tensorflow原始碼解析2 – 前後端連線的橋樑 - Session Tensorflow

Tensorflow原始碼解析2 -- 前後端連線的橋樑 - Session

Tensorflow原始碼解讀系列文章,歡迎閱讀 帶你深入AI(1) - 深度學習模型訓練痛點及解決方法 自然語言處理1 – 分詞 Tensorflow原始碼解析1 – 核心架構和原始碼結構 Tensorflow原始碼解析2 – 前後端連線的橋樑 - Session Tensorflow

Tensorflow原始碼解析5 -- 圖的邊

1 概述 前文兩篇文章分別講解了TensorFlow核心物件Graph,和Graph的節點Operation。Graph另外一大成員,即為其邊Tensor。邊用來表示計算的資料,它經過上游節點計算後得到,然後傳遞給下游節點進行運算。本文講解Graph的邊Ten

Faster R-CNN 原始碼解析Tensorflow版)

演算法原理 Feature extraction + Region proposal network + Classification and regression: 圖片連結 資料生成(imdb, roidb) datasets/im

Tensorflow原始碼解析2 -- 前後端連線的橋樑

1 Session概述 Session是TensorFlow前後端連線的橋樑。使用者利用session使得client能夠與master的執行引擎建立連線,並通過session.run()來觸發一次計算。它建立了一套上下文環境,封裝了operation計算以及

OKHttp原始碼解析(6)----攔截器CallServerInterceptor

系列文章 OKHttp原始碼解析(1)----整體流程 OKHttp原始碼解析(2)----攔截器RetryAndFollowUpInterceptor OKHttp原始碼解析(3)----攔截器BridgeInterceptor OKHttp原始碼解析(4)----攔截器CacheIntercept

RxJava2原始碼解析——基本流程、執行緒排程

本篇文章的目的: ①瞭解RxJava的基本流程 ②瞭解RxJava中執行緒排程的實現 ③瞭解了上面那些,其他的操作符對你來說就不是問題了 RxJava基本流程 我們從基本的使用作為入口: Observable.create(new ObservableOnSubsc

TensorFlow學習筆記(6) TensorFlow最佳實踐樣例程式

在第三篇中編寫了一個程式來解決MNIST問題,這是一個沒有持久化訓練好的模型。當程式退出時,訓練好的模型就再也無法使用了,這導致得到的模型無法被重用。結合變數管理機制及模型持久化機制,對該程式進行進一步的優化重構。 優化重構之後的程式分為三個:第一個是mnist_inference.py,定義前

【Java實戰】原始碼解析為什麼覆蓋equals方法總要覆蓋hashCode方法

1、背景知識本文程式碼基於jdk1.8分析,《Java程式設計思想》中有如下描述:另外再看下Object.java對hashCode()方法的說明:/** * Returns a hash code value for the object. This method

Caffe原始碼解析6:Neuron_Layer

NeuronLayer,顧名思義這裡就是神經元,啟用函式的相應層。我們知道在blob進入啟用函式之前和之後他的size是不會變的,而且啟用值也就是輸出 \(y\) 只依賴於相應的輸入 \(x\)。在Caffe裡面所有的layer的實現都放在src資料夾下的layer資料夾中,基本上很多文章裡應用到的laye

Spring原始碼解析-6、spring單例如何解決迴圈依賴

什麼叫迴圈依賴 迴圈依賴即兩個及以上的bean物件互相持有對方的引用,最終形成一個閉環。 spring如何處理正在建立的Bean Spring容器會將每一個正在建立的Bean 識別符號放在一個“當前建立Bean池”中,Bean識別符號在建立過程中將一直保持 在這個池中,因此如果在

Android 6.0在執行申請許可權解釋與例項

Android 6.0在執行時申請許可權 從android 6.0(API23)開始,當app執行時使用者授予使用者的許可權,而不是在安裝程式的時候。 系統許可權分為2種,分別為normal和dangerous. Normal permission:對於